mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 10:54:44 +00:00
@@ -11,7 +11,7 @@
|
|||||||
"url": "git+https://github.com/robonen/tools.git",
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
"directory": "./apps/vhs"
|
"directory": "./apps/vhs"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.12.0",
|
"packageManager": "pnpm@9.12.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.1.27"
|
"bun": ">=1.1.27"
|
||||||
},
|
},
|
||||||
@@ -21,6 +21,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@robonen/tsconfig": "workspace:*",
|
"@robonen/tsconfig": "workspace:*",
|
||||||
"@types/bun": "^1.1.10"
|
"@types/bun": "^1.1.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,18 +15,18 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/robonen/tools.git"
|
"url": "git+https://github.com/robonen/tools.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.12.0",
|
"packageManager": "pnpm@9.12.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.18.0"
|
"node": ">=20.18.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.16.10",
|
"@types/node": "^20.16.14",
|
||||||
"citty": "^0.1.6",
|
"citty": "^0.1.6",
|
||||||
"jiti": "^2.2.1",
|
"jiti": "^2.3.3",
|
||||||
"pathe": "^1.1.2",
|
"pathe": "^1.1.2",
|
||||||
"scule": "^1.3.0",
|
"scule": "^1.3.0",
|
||||||
"vitepress": "^1.3.4"
|
"vitepress": "^1.4.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"all:build": "pnpm -r build",
|
"all:build": "pnpm -r build",
|
||||||
|
|||||||
9
packages/platform/build.config.ts
Normal file
9
packages/platform/build.config.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineBuildConfig } from 'unbuild';
|
||||||
|
|
||||||
|
export default defineBuildConfig({
|
||||||
|
rollup: {
|
||||||
|
esbuild: {
|
||||||
|
// minify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "https://jsr.io/schema/config-file.v1.json",
|
||||||
"name": "@robonen/platform",
|
"name": "@robonen/platform",
|
||||||
"version": "0.0.0",
|
"license": "Apache-2.0",
|
||||||
|
"version": "0.0.2",
|
||||||
"exports": "./src/index.ts"
|
"exports": "./src/index.ts"
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "@robonen/platform",
|
"name": "@robonen/platform",
|
||||||
"private": true,
|
"version": "0.0.2",
|
||||||
"version": "0.0.0",
|
"license": "Apache-2.0",
|
||||||
"license": "UNLICENSED",
|
"description": "Platform dependent utilities for javascript development",
|
||||||
"description": "",
|
"keywords": [
|
||||||
"keywords": [],
|
"javascript",
|
||||||
|
"typescript",
|
||||||
|
"browser",
|
||||||
|
"platform",
|
||||||
|
"node",
|
||||||
|
"bun",
|
||||||
|
"deno"
|
||||||
|
],
|
||||||
"author": "Robonen Andrew <robonenandrew@gmail.com>",
|
"author": "Robonen Andrew <robonenandrew@gmail.com>",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/robonen/tools.git",
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
"directory": "./packages/platform"
|
"directory": "packages/platform"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.12.0",
|
"packageManager": "pnpm@9.12.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.18.0"
|
"node": ">=20.18.0"
|
||||||
},
|
},
|
||||||
@@ -19,18 +26,21 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"main": "./dist/index.umd.js",
|
"main": "./dist/index.cjs",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.mjs",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.mjs",
|
||||||
"require": "./dist/index.umd.js",
|
"require": "./dist/index.cjs",
|
||||||
"types": "./dist/index.d.ts"
|
"types": "./dist/index.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {},
|
"scripts": {
|
||||||
|
"build": "unbuild"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@robonen/tsconfig": "workspace:*"
|
"@robonen/tsconfig": "workspace:*",
|
||||||
|
"unbuild": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/platform/src/index.ts
Normal file
1
packages/platform/src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './multi';
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
* @category Multi
|
* @category Multi
|
||||||
* @description Global object that works in any environment
|
* @description Global object that works in any environment
|
||||||
*
|
*
|
||||||
* @since 0.0.2
|
* @since 0.0.1
|
||||||
*/
|
*/
|
||||||
export const _global =
|
export const _global =
|
||||||
typeof globalThis !== 'undefined'
|
typeof globalThis !== 'undefined'
|
||||||
@@ -17,3 +17,12 @@ export const _global =
|
|||||||
: typeof self !== 'undefined'
|
: typeof self !== 'undefined'
|
||||||
? self
|
? self
|
||||||
: undefined;
|
: 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';
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "@robonen/tsconfig/tsconfig.json"
|
"extends": "@robonen/tsconfig/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["DOM"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"url": "git+https://github.com/robonen/tools.git",
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
"directory": "packages/renovate"
|
"directory": "packages/renovate"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.12.0",
|
"packageManager": "pnpm@9.12.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.18.0"
|
"node": ">=20.18.0"
|
||||||
},
|
},
|
||||||
@@ -27,6 +27,6 @@
|
|||||||
"test": "renovate-config-validator ./default.json"
|
"test": "renovate-config-validator ./default.json"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"renovate": "^38.110.1"
|
"renovate": "^38.130.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://jsr.io/schema/config-file.v1.json",
|
"$schema": "https://jsr.io/schema/config-file.v1.json",
|
||||||
"name": "@robonen/stdlib",
|
"name": "@robonen/stdlib",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"exports": "./src/index.ts"
|
"exports": "./src/index.ts"
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@robonen/stdlib",
|
"name": "@robonen/stdlib",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"license": "UNLICENSED",
|
"license": "Apache-2.0",
|
||||||
"description": "A collection of tools, utilities, and helpers for TypeScript",
|
"description": "A collection of tools, utilities, and helpers for TypeScript",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"stdlib",
|
"stdlib",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"url": "git+https://github.com/robonen/tools.git",
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
"directory": "packages/stdlib"
|
"directory": "packages/stdlib"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.12.0",
|
"packageManager": "pnpm@9.12.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.18.0"
|
"node": ">=20.18.0"
|
||||||
},
|
},
|
||||||
|
|||||||
0
packages/stdlib/src/async/index.ts
Normal file
0
packages/stdlib/src/async/index.ts
Normal file
3
packages/stdlib/src/async/pool/index.ts
Normal file
3
packages/stdlib/src/async/pool/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export type AsyncPoolOptions = {
|
||||||
|
concurrency?: number;
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Create a function that generates unique flags
|
* @name flagsGenerator
|
||||||
|
* @category Bits
|
||||||
|
* @description Create a function that generates unique flags
|
||||||
*
|
*
|
||||||
* @returns {Function} A function that generates unique flags
|
* @returns {Function} A function that generates unique flags
|
||||||
* @throws {RangeError} If more than 31 flags are created
|
* @throws {RangeError} If more than 31 flags are created
|
||||||
@@ -20,7 +22,9 @@ export function flagsGenerator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to combine multiple flags using the AND operator
|
* @name and
|
||||||
|
* @category Bits
|
||||||
|
* @description Function to combine multiple flags using the AND operator
|
||||||
*
|
*
|
||||||
* @param {number[]} flags - The flags to combine
|
* @param {number[]} flags - The flags to combine
|
||||||
* @returns {number} The combined flags
|
* @returns {number} The combined flags
|
||||||
@@ -32,7 +36,9 @@ export function and(...flags: number[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to combine multiple flags using the OR operator
|
* @name or
|
||||||
|
* @category Bits
|
||||||
|
* @description Function to combine multiple flags using the OR operator
|
||||||
*
|
*
|
||||||
* @param {number[]} flags - The flags to combine
|
* @param {number[]} flags - The flags to combine
|
||||||
* @returns {number} The combined flags
|
* @returns {number} The combined flags
|
||||||
@@ -44,7 +50,9 @@ export function or(...flags: number[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to apply the NOT operator to a flag
|
* @name not
|
||||||
|
* @category Bits
|
||||||
|
* @description Function to combine multiple flags using the XOR operator
|
||||||
*
|
*
|
||||||
* @param {number} flag - The flag to apply the NOT operator to
|
* @param {number} flag - The flag to apply the NOT operator to
|
||||||
* @returns {number} The result of the NOT operator
|
* @returns {number} The result of the NOT operator
|
||||||
@@ -56,7 +64,9 @@ export function not(flag: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to make sure a flag has a specific bit set
|
* @name has
|
||||||
|
* @category Bits
|
||||||
|
* @description Function to make sure a flag has a specific bit set
|
||||||
*
|
*
|
||||||
* @param {number} flag - The flag to check
|
* @param {number} flag - The flag to check
|
||||||
* @param {number} other - Flag to check
|
* @param {number} other - Flag to check
|
||||||
@@ -69,7 +79,9 @@ export function has(flag: number, other: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to check if a flag is set
|
* @name is
|
||||||
|
* @category Bits
|
||||||
|
* @description Function to check if a flag is set
|
||||||
*
|
*
|
||||||
* @param {number} flag - The flag to check
|
* @param {number} flag - The flag to check
|
||||||
* @returns {boolean} Whether the flag is set
|
* @returns {boolean} Whether the flag is set
|
||||||
@@ -81,7 +93,9 @@ export function is(flag: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to unset a flag
|
* @name unset
|
||||||
|
* @category Bits
|
||||||
|
* @description Function to unset a flag
|
||||||
*
|
*
|
||||||
* @param {number} flag - Source flag
|
* @param {number} flag - Source flag
|
||||||
* @param {number} other - Flag to unset
|
* @param {number} other - Flag to unset
|
||||||
@@ -94,7 +108,9 @@ export function unset(flag: number, other: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to toggle (xor) a flag
|
* @name toggle
|
||||||
|
* @category Bits
|
||||||
|
* @description Function to toggle (xor) a flag
|
||||||
*
|
*
|
||||||
* @param {number} flag - Source flag
|
* @param {number} flag - Source flag
|
||||||
* @param {number} other - Flag to toggle
|
* @param {number} other - Flag to toggle
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export * from './bits';
|
|||||||
export * from './structs';
|
export * from './structs';
|
||||||
export * from './arrays';
|
export * from './arrays';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
export * from './utils'
|
||||||
|
|||||||
@@ -43,4 +43,39 @@ describe('clamp', () => {
|
|||||||
// negative range and value
|
// negative range and value
|
||||||
expect(clamp(-10, -100, -5)).toBe(-10);
|
expect(clamp(-10, -100, -5)).toBe(-10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handle NaN and Infinity', () => {
|
||||||
|
// value is NaN
|
||||||
|
expect(clamp(NaN, 0, 100)).toBe(NaN);
|
||||||
|
|
||||||
|
// min is NaN
|
||||||
|
expect(clamp(50, NaN, 100)).toBe(NaN);
|
||||||
|
|
||||||
|
// max is NaN
|
||||||
|
expect(clamp(50, 0, NaN)).toBe(NaN);
|
||||||
|
|
||||||
|
// value is Infinity
|
||||||
|
expect(clamp(Infinity, 0, 100)).toBe(100);
|
||||||
|
|
||||||
|
// min is Infinity
|
||||||
|
expect(clamp(50, Infinity, 100)).toBe(100);
|
||||||
|
|
||||||
|
// max is Infinity
|
||||||
|
expect(clamp(50, 0, Infinity)).toBe(50);
|
||||||
|
|
||||||
|
// min and max are Infinity
|
||||||
|
expect(clamp(50, Infinity, Infinity)).toBe(Infinity);
|
||||||
|
|
||||||
|
// value is -Infinity
|
||||||
|
expect(clamp(-Infinity, 0, 100)).toBe(0);
|
||||||
|
|
||||||
|
// min is -Infinity
|
||||||
|
expect(clamp(50, -Infinity, 100)).toBe(50);
|
||||||
|
|
||||||
|
// max is -Infinity
|
||||||
|
expect(clamp(50, 0, -Infinity)).toBe(-Infinity);
|
||||||
|
|
||||||
|
// min and max are -Infinity
|
||||||
|
expect(clamp(50, -Infinity, -Infinity)).toBe(-Infinity);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Clamps a number between a minimum and maximum value
|
* @name clamp
|
||||||
|
* @category Math
|
||||||
|
* @description Clamps a number between a minimum and maximum value
|
||||||
*
|
*
|
||||||
* @param {number} value The number to clamp
|
* @param {number} value The number to clamp
|
||||||
* @param {number} min Minimum value
|
* @param {number} min Minimum value
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {describe, it, expect} from 'vitest';
|
import {describe, it, expect} from 'vitest';
|
||||||
import {inverseLerp, lerp} from './index';
|
import {inverseLerp, lerp} from '.';
|
||||||
|
|
||||||
describe('lerp', () => {
|
describe('lerp', () => {
|
||||||
it('interpolates between two values', () => {
|
it('interpolates between two values', () => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Linearly interpolates between two values
|
* @name lerp
|
||||||
|
* @category Math
|
||||||
|
* @description Linearly interpolates between two values
|
||||||
*
|
*
|
||||||
* @param {number} start The start value
|
* @param {number} start The start value
|
||||||
* @param {number} end The end value
|
* @param {number} end The end value
|
||||||
@@ -13,7 +15,9 @@ export function lerp(start: number, end: number, t: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inverse linear interpolation between two values
|
* @name inverseLerp
|
||||||
|
* @category Math
|
||||||
|
* @description Inverse linear interpolation between two values
|
||||||
*
|
*
|
||||||
* @param {number} start The start value
|
* @param {number} start The start value
|
||||||
* @param {number} end The end value
|
* @param {number} end The end value
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {describe, expect, it} from 'vitest';
|
import {describe, expect, it} from 'vitest';
|
||||||
import {remap} from './index';
|
import {remap} from '.';
|
||||||
|
|
||||||
describe('remap', () => {
|
describe('remap', () => {
|
||||||
it('map values from one range to another', () => {
|
it('map values from one range to another', () => {
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { clamp } from '../clamp';
|
|||||||
import {inverseLerp, lerp} from '../lerp';
|
import {inverseLerp, lerp} from '../lerp';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map a value from one range to another
|
* @name remap
|
||||||
|
* @category Math
|
||||||
|
* @description Map a value from one range to another
|
||||||
*
|
*
|
||||||
* @param {number} value The value to map
|
* @param {number} value The value to map
|
||||||
* @param {number} in_min The minimum value of the input range
|
* @param {number} in_min The minimum value of the input range
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {describe, it, expect} from 'vitest';
|
import {describe, it, expect} from 'vitest';
|
||||||
import {clampBigInt} from './index';
|
import {clampBigInt} from '.';
|
||||||
|
|
||||||
describe('clampBigInt', () => {
|
describe('clampBigInt', () => {
|
||||||
it('clamp a value within the given range', () => {
|
it('clamp a value within the given range', () => {
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import {minBigInt} from '../minBigInt';
|
|||||||
import {maxBigInt} from '../maxBigInt';
|
import {maxBigInt} from '../maxBigInt';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clamps a bigint between a minimum and maximum value
|
* @name clampBigInt
|
||||||
|
* @category Math
|
||||||
|
* @description Clamps a bigint between a minimum and maximum value
|
||||||
*
|
*
|
||||||
* @param {bigint} value The number to clamp
|
* @param {bigint} value The number to clamp
|
||||||
* @param {bigint} min Minimum value
|
* @param {bigint} min Minimum value
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {describe, it, expect} from 'vitest';
|
import {describe, it, expect} from 'vitest';
|
||||||
import {inverseLerpBigInt, lerpBigInt} from './index';
|
import {inverseLerpBigInt, lerpBigInt} from '.';
|
||||||
|
|
||||||
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
|
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Linearly interpolates between bigint values
|
* @name lerpBigInt
|
||||||
|
* @category Math
|
||||||
|
* @description Linearly interpolates between bigint values
|
||||||
*
|
*
|
||||||
* @param {bigint} start The start value
|
* @param {bigint} start The start value
|
||||||
* @param {bigint} end The end value
|
* @param {bigint} end The end value
|
||||||
@@ -13,7 +15,9 @@ export function lerpBigInt(start: bigint, end: bigint, t: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inverse linear interpolation between two bigint values
|
* @name inverseLerpBigInt
|
||||||
|
* @category Math
|
||||||
|
* @description Inverse linear interpolation between two bigint values
|
||||||
*
|
*
|
||||||
* @param {bigint} start The start value
|
* @param {bigint} start The start value
|
||||||
* @param {bigint} end The end value
|
* @param {bigint} end The end value
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { maxBigInt } from './index';
|
import { maxBigInt } from '.';
|
||||||
|
|
||||||
describe('maxBigInt', () => {
|
describe('maxBigInt', () => {
|
||||||
it('returns -Infinity when no values are provided', () => {
|
it('returns -Infinity when no values are provided', () => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Like `Math.max` but for BigInts
|
* @name maxBigInt
|
||||||
|
* @category Math
|
||||||
|
* @description Like `Math.max` but for BigInts
|
||||||
*
|
*
|
||||||
* @param {...bigint} values The values to compare
|
* @param {...bigint} values The values to compare
|
||||||
* @returns {bigint} The largest value
|
* @returns {bigint} The largest value
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {describe, it, expect} from 'vitest';
|
import {describe, it, expect} from 'vitest';
|
||||||
import {minBigInt} from './index';
|
import {minBigInt} from '.';
|
||||||
|
|
||||||
describe('minBigInt', () => {
|
describe('minBigInt', () => {
|
||||||
it('returns Infinity when no values are provided', () => {
|
it('returns Infinity when no values are provided', () => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Like `Math.min` but for BigInts
|
* @name minBigInt
|
||||||
|
* @category Math
|
||||||
|
* @description Like `Math.min` but for BigInts
|
||||||
*
|
*
|
||||||
* @param {...bigint} values The values to compare
|
* @param {...bigint} values The values to compare
|
||||||
* @returns {bigint} The smallest value
|
* @returns {bigint} The smallest value
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {describe, expect, it} from 'vitest';
|
import {describe, expect, it} from 'vitest';
|
||||||
import {remapBigInt} from './index';
|
import {remapBigInt} from '.';
|
||||||
|
|
||||||
describe('remapBigInt', () => {
|
describe('remapBigInt', () => {
|
||||||
it('map values from one range to another', () => {
|
it('map values from one range to another', () => {
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { clampBigInt } from '../clampBigInt';
|
|||||||
import {inverseLerpBigInt, lerpBigInt} from '../lerpBigInt';
|
import {inverseLerpBigInt, lerpBigInt} from '../lerpBigInt';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map a bigint value from one range to another
|
* @name remapBigInt
|
||||||
|
* @category Math
|
||||||
|
* @description Map a bigint value from one range to another
|
||||||
*
|
*
|
||||||
* @param {bigint} value The value to map
|
* @param {bigint} value The value to map
|
||||||
* @param {bigint} in_min The minimum value of the input range
|
* @param {bigint} in_min The minimum value of the input range
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { PubSub } from './index';
|
import { PubSub } from '.';
|
||||||
|
|
||||||
describe('pubsub', () => {
|
describe('pubsub', () => {
|
||||||
|
const event3 = Symbol('event3');
|
||||||
|
|
||||||
let eventBus: PubSub<{
|
let eventBus: PubSub<{
|
||||||
event1: (arg: string) => void;
|
event1: (arg: string) => void;
|
||||||
event2: () => void
|
event2: () => void;
|
||||||
|
[event3]: () => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -32,6 +35,15 @@ describe('pubsub', () => {
|
|||||||
expect(listener2).toHaveBeenCalledWith('Hello');
|
expect(listener2).toHaveBeenCalledWith('Hello');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('emit symbol event', () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
|
||||||
|
eventBus.on(event3, listener);
|
||||||
|
eventBus.emit(event3);
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('add a one-time listener and emit an event', () => {
|
it('add a one-time listener and emit an event', () => {
|
||||||
const listener = vi.fn();
|
const listener = vi.fn();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
export type Subscriber = (...args: any[]) => void;
|
export type Subscriber = (...args: any[]) => void;
|
||||||
export type EventsRecord = Record<string, Subscriber>;
|
export type EventsRecord = Record<string | symbol, Subscriber>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple PubSub implementation
|
* @name PubSub
|
||||||
|
* @category Patterns
|
||||||
|
* @description Simple PubSub implementation
|
||||||
*
|
*
|
||||||
* @since 0.0.2
|
* @since 0.0.2
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { Stack } from './index';
|
import { Stack } from '.';
|
||||||
|
|
||||||
describe('stack', () => {
|
describe('stack', () => {
|
||||||
describe('constructor', () => {
|
describe('constructor', () => {
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ export type StackOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a stack data structure
|
* @name Stack
|
||||||
|
* @category Data Structures
|
||||||
|
* @description Represents a stack data structure
|
||||||
*
|
*
|
||||||
* @since 0.0.2
|
* @since 0.0.2
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Calculate the Levenshtein distance between two strings
|
* @name levenshteinDistance
|
||||||
|
* @category Text
|
||||||
|
* @description Calculate the Levenshtein distance between two strings
|
||||||
*
|
*
|
||||||
* @param {string} left First string
|
* @param {string} left First string
|
||||||
* @param {string} right Second string
|
* @param {string} right Second string
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { templateObject } from './index';
|
import { templateObject } from '.';
|
||||||
|
|
||||||
describe('templateObject', () => {
|
describe('templateObject', () => {
|
||||||
// it('replace template placeholders with corresponding values from args', () => {
|
// it('replace template placeholders with corresponding values from args', () => {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
export type Trigrams = Map<string, number>;
|
export type Trigrams = Map<string, number>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts trigrams from a text and returns a map of trigram to count
|
* @name trigramProfile
|
||||||
|
* @category Text
|
||||||
|
* @description Extracts trigrams from a text and returns a map of trigram to count
|
||||||
*
|
*
|
||||||
* @param {string} text The text to extract trigrams
|
* @param {string} text The text to extract trigrams
|
||||||
* @returns {Trigrams} A map of trigram to count
|
* @returns {Trigrams} A map of trigram to count
|
||||||
@@ -23,7 +25,9 @@ export function trigramProfile(text: string): Trigrams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the trigram distance between two strings
|
* @name trigramDistance
|
||||||
|
* @category Text
|
||||||
|
* @description Calculates the trigram distance between two strings
|
||||||
*
|
*
|
||||||
* @param {Trigrams} left First text trigram profile
|
* @param {Trigrams} left First text trigram profile
|
||||||
* @param {Trigrams} right Second text trigram profile
|
* @param {Trigrams} right Second text trigram profile
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* To string any value.
|
* @name toString
|
||||||
|
* @category Types
|
||||||
|
* @description To string any value
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const toString = (value: any): string => Object.prototype.toString.call(value);
|
export const toString = (value: any): string => Object.prototype.toString.call(value);
|
||||||
@@ -1,81 +1,121 @@
|
|||||||
import { toString } from '.';
|
import { toString } from '.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is an array.
|
* @name isFunction
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is an array
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is any[]}
|
* @returns {value is any[]}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isArray = (value: any): value is any[] => Array.isArray(value);
|
export const isArray = (value: any): value is any[] => Array.isArray(value);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is an object.
|
* @name isObject
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is an object
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is object}
|
* @returns {value is object}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isObject = (value: any): value is object => toString(value) === '[object Object]';
|
export const isObject = (value: any): value is object => toString(value) === '[object Object]';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a regexp.
|
* @name isRegExp
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a regexp
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is RegExp}
|
* @returns {value is RegExp}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isRegExp = (value: any): value is RegExp => toString(value) === '[object RegExp]';
|
export const isRegExp = (value: any): value is RegExp => toString(value) === '[object RegExp]';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a date.
|
* @name isDate
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a date
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is Date}
|
* @returns {value is Date}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isDate = (value: any): value is Date => toString(value) === '[object Date]';
|
export const isDate = (value: any): value is Date => toString(value) === '[object Date]';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is an error.
|
* @name isError
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is an error
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is Error}
|
* @returns {value is Error}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isError = (value: any): value is Error => toString(value) === '[object Error]';
|
export const isError = (value: any): value is Error => toString(value) === '[object Error]';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a promise.
|
* @name isPromise
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a promise
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is Promise<any>}
|
* @returns {value is Promise<any>}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isPromise = (value: any): value is Promise<any> => toString(value) === '[object Promise]';
|
export const isPromise = (value: any): value is Promise<any> => toString(value) === '[object Promise]';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a map.
|
* @name isMap
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a map
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is Map<any, any>}
|
* @returns {value is Map<any, any>}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isMap = (value: any): value is Map<any, any> => toString(value) === '[object Map]';
|
export const isMap = (value: any): value is Map<any, any> => toString(value) === '[object Map]';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a set.
|
* @name isSet
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a set
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is Set<any>}
|
* @returns {value is Set<any>}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isSet = (value: any): value is Set<any> => toString(value) === '[object Set]';
|
export const isSet = (value: any): value is Set<any> => toString(value) === '[object Set]';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a weakmap.
|
* @name isWeakMap
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a weakmap
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is WeakMap<object, any>}
|
* @returns {value is WeakMap<object, any>}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isWeakMap = (value: any): value is WeakMap<object, any> => toString(value) === '[object WeakMap]';
|
export const isWeakMap = (value: any): value is WeakMap<object, any> => toString(value) === '[object WeakMap]';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a weakset.
|
* @name isWeakSet
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a weakset
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is WeakSet<object>}
|
* @returns {value is WeakSet<object>}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isWeakSet = (value: any): value is WeakSet<object> => toString(value) === '[object WeakSet]';
|
export const isWeakSet = (value: any): value is WeakSet<object> => toString(value) === '[object WeakSet]';
|
||||||
|
|||||||
@@ -1,65 +1,97 @@
|
|||||||
import { toString } from '.';
|
import { toString } from '.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a boolean.
|
* @name isObject
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a boolean
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is boolean}
|
* @returns {value is boolean}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isBoolean = (value: any): value is boolean => typeof value === 'boolean';
|
export const isBoolean = (value: any): value is boolean => typeof value === 'boolean';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a function.
|
* @name isFunction
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a function
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is Function}
|
* @returns {value is Function}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isFunction = <T extends Function>(value: any): value is T => typeof value === 'function';
|
export const isFunction = <T extends Function>(value: any): value is T => typeof value === 'function';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a number.
|
* @name isNumber
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a number
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is number}
|
* @returns {value is number}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isNumber = (value: any): value is number => typeof value === 'number';
|
export const isNumber = (value: any): value is number => typeof value === 'number';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a bigint.
|
* @name isBigInt
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a bigint
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is bigint}
|
* @returns {value is bigint}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isBigInt = (value: any): value is bigint => typeof value === 'bigint';
|
export const isBigInt = (value: any): value is bigint => typeof value === 'bigint';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a string.
|
* @name isString
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a string
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is string}
|
* @returns {value is string}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isString = (value: any): value is string => typeof value === 'string';
|
export const isString = (value: any): value is string => typeof value === 'string';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a symbol.
|
* @name isSymbol
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a symbol
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is symbol}
|
* @returns {value is symbol}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isSymbol = (value: any): value is symbol => typeof value === 'symbol';
|
export const isSymbol = (value: any): value is symbol => typeof value === 'symbol';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a undefined.
|
* @name isUndefined
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a undefined
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is undefined}
|
* @returns {value is undefined}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isUndefined = (value: any): value is undefined => toString(value) === '[object Undefined]';
|
export const isUndefined = (value: any): value is undefined => toString(value) === '[object Undefined]';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a null.
|
* @name isNull
|
||||||
|
* @category Types
|
||||||
|
* @description Check if a value is a null
|
||||||
*
|
*
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns {value is null}
|
* @returns {value is null}
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isNull = (value: any): value is null => toString(value) === '[object Null]';
|
export const isNull = (value: any): value is null => toString(value) === '[object Null]';
|
||||||
|
|||||||
4
packages/stdlib/src/types/ts/array.ts
Normal file
4
packages/stdlib/src/types/ts/array.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* A type that can be either a single value or an array of values
|
||||||
|
*/
|
||||||
|
export type Arrayable<T> = T | T[];
|
||||||
9
packages/stdlib/src/types/ts/function.ts
Normal file
9
packages/stdlib/src/types/ts/function.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Any function
|
||||||
|
*/
|
||||||
|
export type AnyFunction = (...args: any[]) => any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Void function
|
||||||
|
*/
|
||||||
|
export type VoidFunction = () => void;
|
||||||
@@ -1 +1,3 @@
|
|||||||
export * from './string';
|
export * from './string';
|
||||||
|
export * from './function';
|
||||||
|
export * from './array';
|
||||||
21
packages/stdlib/src/utils/index.ts
Normal file
21
packages/stdlib/src/utils/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* @name timestamp
|
||||||
|
* @category Utils
|
||||||
|
* @description Returns the current timestamp
|
||||||
|
*
|
||||||
|
* @returns {number} The current timestamp
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
|
*/
|
||||||
|
export const timestamp = () => Date.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name noop
|
||||||
|
* @category Utils
|
||||||
|
* @description A function that does nothing
|
||||||
|
*
|
||||||
|
* @returns {void} Nothing
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
|
*/
|
||||||
|
export const noop = () => {};
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"url": "git+https://github.com/robonen/tools.git",
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
"directory": "packages/tsconfig"
|
"directory": "packages/tsconfig"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.12.0",
|
"packageManager": "pnpm@9.12.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.18.0"
|
"node": ">=20.18.0"
|
||||||
},
|
},
|
||||||
|
|||||||
10
packages/vue/build.config.ts
Normal file
10
packages/vue/build.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineBuildConfig } from 'unbuild';
|
||||||
|
|
||||||
|
export default defineBuildConfig({
|
||||||
|
externals: ['vue'],
|
||||||
|
rollup: {
|
||||||
|
esbuild: {
|
||||||
|
// minify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "https://jsr.io/schema/config-file.v1.json",
|
||||||
"name": "@robonen/vue",
|
"name": "@robonen/vue",
|
||||||
"version": "0.0.0",
|
"license": "Apache-2.0",
|
||||||
|
"version": "0.0.1",
|
||||||
"exports": "./src/index.ts"
|
"exports": "./src/index.ts"
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "@robonen/vue",
|
"name": "@robonen/vue",
|
||||||
"private": true,
|
"version": "0.0.1",
|
||||||
"version": "0.0.0",
|
"license": "Apache-2.0",
|
||||||
"license": "UNLICENSED",
|
"description": "Collection of powerful tools for Vue",
|
||||||
"description": "",
|
"keywords": [
|
||||||
"keywords": [],
|
"vue",
|
||||||
|
"tools",
|
||||||
|
"ui",
|
||||||
|
"utilities",
|
||||||
|
"composables"
|
||||||
|
],
|
||||||
"author": "Robonen Andrew <robonenandrew@gmail.com>",
|
"author": "Robonen Andrew <robonenandrew@gmail.com>",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/robonen/tools.git",
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
"directory": "./packages/vue"
|
"directory": "./packages/vue"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.12.0",
|
"packageManager": "pnpm@9.12.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.18.0"
|
"node": ">=20.18.0"
|
||||||
},
|
},
|
||||||
@@ -19,27 +24,30 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"main": "./dist/index.umd.js",
|
"main": "./dist/index.cjs",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.mjs",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.mjs",
|
||||||
"require": "./dist/index.umd.js",
|
"require": "./dist/index.cjs",
|
||||||
"types": "./dist/index.d.ts"
|
"types": "./dist/index.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"dev": "vitest dev"
|
"dev": "vitest dev",
|
||||||
|
"build": "unbuild"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@robonen/tsconfig": "workspace:*",
|
"@robonen/tsconfig": "workspace:*",
|
||||||
"@vue/test-utils": "catalog:",
|
"@vue/test-utils": "catalog:",
|
||||||
"jsdom": "catalog:",
|
"jsdom": "catalog:",
|
||||||
|
"unbuild": "catalog:",
|
||||||
"vitest": "catalog:"
|
"vitest": "catalog:"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@robonen/platform": "workspace:*",
|
||||||
"@robonen/stdlib": "workspace:*",
|
"@robonen/stdlib": "workspace:*",
|
||||||
"vue": "catalog:"
|
"vue": "catalog:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
|
export * from './tryOnBeforeMount';
|
||||||
|
export * from './tryOnMounted';
|
||||||
|
export * from './tryOnScopeDispose';
|
||||||
|
export * from './useAppSharedState';
|
||||||
export * from './useCached';
|
export * from './useCached';
|
||||||
|
export * from './useClamp';
|
||||||
|
export * from './useContextFactory';
|
||||||
export * from './useCounter';
|
export * from './useCounter';
|
||||||
|
export * from './useLastChanged';
|
||||||
|
export * from './useMounted';
|
||||||
|
export * from './useOffsetPagination';
|
||||||
export * from './useRenderCount';
|
export * from './useRenderCount';
|
||||||
|
export * from './useRenderInfo';
|
||||||
|
export * from './useSupported';
|
||||||
export * from './useSyncRefs';
|
export * from './useSyncRefs';
|
||||||
export * from './useToggle';
|
|
||||||
45
packages/vue/src/composables/tryOnBeforeMount/index.ts
Normal file
45
packages/vue/src/composables/tryOnBeforeMount/index.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { onBeforeMount, nextTick, type ComponentInternalInstance } from 'vue';
|
||||||
|
import { getLifeCycleTarger } from '../..';
|
||||||
|
import type { VoidFunction } from '@robonen/stdlib';
|
||||||
|
|
||||||
|
// TODO: test
|
||||||
|
|
||||||
|
export interface TryOnBeforeMountOptions {
|
||||||
|
sync?: boolean;
|
||||||
|
target?: ComponentInternalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name tryOnBeforeMount
|
||||||
|
* @category Components
|
||||||
|
* @description Call onBeforeMount if it's inside a component lifecycle hook, otherwise just calls it
|
||||||
|
*
|
||||||
|
* @param {VoidFunction} fn - The function to run on before mount.
|
||||||
|
* @param {TryOnBeforeMountOptions} options - The options for the function.
|
||||||
|
* @param {boolean} [options.sync=true] - If true, the function will run synchronously, otherwise it will run asynchronously.
|
||||||
|
* @param {ComponentInternalInstance} [options.target] - The target component instance to run the function on.
|
||||||
|
* @returns {void}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* tryOnBeforeMount(() => console.log('Before mount'));
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* tryOnBeforeMount(() => console.log('Before mount async'), { sync: false });
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export function tryOnBeforeMount(fn: VoidFunction, options: TryOnBeforeMountOptions = {}) {
|
||||||
|
const {
|
||||||
|
sync = true,
|
||||||
|
target,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const instance = getLifeCycleTarger(target);
|
||||||
|
|
||||||
|
if (instance)
|
||||||
|
onBeforeMount(fn, instance);
|
||||||
|
else if (sync)
|
||||||
|
fn();
|
||||||
|
else
|
||||||
|
nextTick(fn);
|
||||||
|
}
|
||||||
57
packages/vue/src/composables/tryOnMounted/index.test.ts
Normal file
57
packages/vue/src/composables/tryOnMounted/index.test.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, it, vi, expect } from 'vitest';
|
||||||
|
import { defineComponent, nextTick, type PropType } from 'vue';
|
||||||
|
import { tryOnMounted } from '.';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import type { VoidFunction } from '@robonen/stdlib';
|
||||||
|
|
||||||
|
const ComponentStub = defineComponent({
|
||||||
|
props: {
|
||||||
|
callback: {
|
||||||
|
type: Function as PropType<VoidFunction>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
props.callback && tryOnMounted(props.callback);
|
||||||
|
},
|
||||||
|
template: `<div></div>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tryOnMounted', () => {
|
||||||
|
it('run the callback when mounted', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
mount(ComponentStub, {
|
||||||
|
props: { callback },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('run the callback outside of a component lifecycle', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
tryOnMounted(callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('run the callback asynchronously', async () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
tryOnMounted(callback, { sync: false });
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
await nextTick();
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('run the callback with a specific target', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const component = mount(ComponentStub);
|
||||||
|
|
||||||
|
tryOnMounted(callback, { target: component.vm.$ });
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
45
packages/vue/src/composables/tryOnMounted/index.ts
Normal file
45
packages/vue/src/composables/tryOnMounted/index.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { onMounted, nextTick, type ComponentInternalInstance } from 'vue';
|
||||||
|
import { getLifeCycleTarger } from '../..';
|
||||||
|
import type { VoidFunction } from '@robonen/stdlib';
|
||||||
|
|
||||||
|
// TODO: tests
|
||||||
|
|
||||||
|
export interface TryOnMountedOptions {
|
||||||
|
sync?: boolean;
|
||||||
|
target?: ComponentInternalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name tryOnMounted
|
||||||
|
* @category Components
|
||||||
|
* @description Call onMounted if it's inside a component lifecycle hook, otherwise just calls it
|
||||||
|
*
|
||||||
|
* @param {VoidFunction} fn The function to call
|
||||||
|
* @param {TryOnMountedOptions} options The options to use
|
||||||
|
* @param {boolean} [options.sync=true] If the function should be called synchronously
|
||||||
|
* @param {ComponentInternalInstance} [options.target] The target instance to use
|
||||||
|
* @returns {void}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* tryOnMounted(() => console.log('Mounted!'));
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* tryOnMounted(() => console.log('Mounted!'), { sync: false });
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export function tryOnMounted(fn: VoidFunction, options: TryOnMountedOptions = {}) {
|
||||||
|
const {
|
||||||
|
sync = true,
|
||||||
|
target,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const instance = getLifeCycleTarger(target);
|
||||||
|
|
||||||
|
if (instance)
|
||||||
|
onMounted(fn, instance);
|
||||||
|
else if (sync)
|
||||||
|
fn();
|
||||||
|
else
|
||||||
|
nextTick(fn);
|
||||||
|
}
|
||||||
58
packages/vue/src/composables/tryOnScopeDispose/index.test.ts
Normal file
58
packages/vue/src/composables/tryOnScopeDispose/index.test.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { defineComponent, effectScope, type PropType } from 'vue';
|
||||||
|
import { tryOnScopeDispose } from '.';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import type { VoidFunction } from '@robonen/stdlib';
|
||||||
|
|
||||||
|
const ComponentStub = defineComponent({
|
||||||
|
props: {
|
||||||
|
callback: {
|
||||||
|
type: Function as PropType<VoidFunction>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
tryOnScopeDispose(props.callback);
|
||||||
|
},
|
||||||
|
template: '<div></div>',
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tryOnScopeDispose', () => {
|
||||||
|
it('returns false when the scope is not active', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
const detectedScope = tryOnScopeDispose(callback);
|
||||||
|
|
||||||
|
expect(detectedScope).toBe(false);
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('run the callback when the scope is disposed', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
const scope = effectScope();
|
||||||
|
let detectedScope: boolean | undefined;
|
||||||
|
|
||||||
|
scope.run(() => {
|
||||||
|
detectedScope = tryOnScopeDispose(callback);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(detectedScope).toBe(true);
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
scope.stop();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('run callback when the component is unmounted', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
const component = mount(ComponentStub, {
|
||||||
|
props: { callback },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
component.unmount();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
24
packages/vue/src/composables/tryOnScopeDispose/index.ts
Normal file
24
packages/vue/src/composables/tryOnScopeDispose/index.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { VoidFunction } from '@robonen/stdlib';
|
||||||
|
import { getCurrentScope, onScopeDispose } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name tryOnScopeDispose
|
||||||
|
* @category Components
|
||||||
|
* @description A composable that will run a callback when the scope is disposed or do nothing if the scope isn't available.
|
||||||
|
*
|
||||||
|
* @param {VoidFunction} callback - The callback to run when the scope is disposed.
|
||||||
|
* @returns {boolean} - Returns true if the callback was run, otherwise false.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* tryOnScopeDispose(() => console.log('Scope disposed'));
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export function tryOnScopeDispose(callback: VoidFunction) {
|
||||||
|
if (getCurrentScope()) {
|
||||||
|
onScopeDispose(callback);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
40
packages/vue/src/composables/useAppSharedState/index.test.ts
Normal file
40
packages/vue/src/composables/useAppSharedState/index.test.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, it, vi, expect } from 'vitest';
|
||||||
|
import { ref, reactive } from 'vue';
|
||||||
|
import { useAppSharedState } from '.';
|
||||||
|
|
||||||
|
describe('useAppSharedState', () => {
|
||||||
|
it('initialize state only once', () => {
|
||||||
|
const stateFactory = (initValue?: number) => {
|
||||||
|
const count = ref(initValue ?? 0);
|
||||||
|
return { count };
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSharedState = useAppSharedState(stateFactory);
|
||||||
|
|
||||||
|
const state1 = useSharedState(1);
|
||||||
|
const state2 = useSharedState(2);
|
||||||
|
|
||||||
|
expect(state1.count.value).toBe(1);
|
||||||
|
expect(state2.count.value).toBe(1);
|
||||||
|
expect(state1).toBe(state2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return the same state object across different calls', () => {
|
||||||
|
const stateFactory = () => {
|
||||||
|
const state = reactive({ count: 0 });
|
||||||
|
const increment = () => state.count++;
|
||||||
|
return { state, increment };
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSharedState = useAppSharedState(stateFactory);
|
||||||
|
|
||||||
|
const sharedState1 = useSharedState();
|
||||||
|
const sharedState2 = useSharedState();
|
||||||
|
|
||||||
|
expect(sharedState1.state.count).toBe(0);
|
||||||
|
sharedState1.increment();
|
||||||
|
expect(sharedState1.state.count).toBe(1);
|
||||||
|
expect(sharedState2.state.count).toBe(1);
|
||||||
|
expect(sharedState1).toBe(sharedState2);
|
||||||
|
});
|
||||||
|
});
|
||||||
42
packages/vue/src/composables/useAppSharedState/index.ts
Normal file
42
packages/vue/src/composables/useAppSharedState/index.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { AnyFunction } from '@robonen/stdlib';
|
||||||
|
import { effectScope } from 'vue';
|
||||||
|
|
||||||
|
// TODO: maybe we should control subscriptions and dispose them when the child scope is disposed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useAppSharedState
|
||||||
|
* @category State
|
||||||
|
* @description Provides a shared state object for use across Vue instances
|
||||||
|
*
|
||||||
|
* @param {Function} stateFactory A factory function that returns the shared state object
|
||||||
|
* @returns {Function} A function that returns the shared state object
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const useSharedState = useAppSharedState((initValue?: number) => {
|
||||||
|
* const count = ref(initValue ?? 0);
|
||||||
|
* return { count };
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const useSharedState = useAppSharedState(() => {
|
||||||
|
* const state = reactive({ count: 0 });
|
||||||
|
* const increment = () => state.count++;
|
||||||
|
* return { state, increment };
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export function useAppSharedState<Fn extends AnyFunction>(stateFactory: Fn) {
|
||||||
|
let initialized = false;
|
||||||
|
let state: ReturnType<Fn>;
|
||||||
|
const scope = effectScope(true);
|
||||||
|
|
||||||
|
return ((...args: Parameters<Fn>) => {
|
||||||
|
if (!initialized) {
|
||||||
|
state = scope.run(() => stateFactory(...args));
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { ref, nextTick } from 'vue';
|
import { ref, nextTick, reactive } from 'vue';
|
||||||
import { useCached } from '.';
|
import { useCached } from '.';
|
||||||
|
|
||||||
const arrayEquals = (a: number[], b: number[]) => a.length === b.length && a.every((v, i) => v === b[i]);
|
const arrayEquals = (a: number[], b: number[]) => a.length === b.length && a.every((v, i) => v === b[i]);
|
||||||
@@ -14,10 +14,6 @@ describe('useCached', () => {
|
|||||||
externalValue.value = 1;
|
externalValue.value = 1;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(cachedValue.value).toBe(1);
|
expect(cachedValue.value).toBe(1);
|
||||||
|
|
||||||
externalValue.value = 10;
|
|
||||||
await nextTick();
|
|
||||||
expect(cachedValue.value).toBe(10);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('custom array comparator', async () => {
|
it('custom array comparator', async () => {
|
||||||
@@ -41,4 +37,15 @@ describe('useCached', () => {
|
|||||||
expect(cachedValue.value).not.toEqual(initialValue);
|
expect(cachedValue.value).not.toEqual(initialValue);
|
||||||
expect(cachedValue.value).toEqual([2]);
|
expect(cachedValue.value).toEqual([2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('getter source', async () => {
|
||||||
|
const externalValue = reactive({ value: 0 });
|
||||||
|
const cachedValue = useCached(() => externalValue.value);
|
||||||
|
|
||||||
|
expect(cachedValue.value).toBe(0);
|
||||||
|
|
||||||
|
externalValue.value = 1;
|
||||||
|
await nextTick();
|
||||||
|
expect(cachedValue.value).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ref, watch, type Ref, type WatchOptions } from 'vue';
|
import { ref, watch, toValue, type MaybeRefOrGetter, type Ref, type WatchOptions } from 'vue';
|
||||||
|
|
||||||
export type Comparator<Value> = (a: Value, b: Value) => boolean;
|
export type Comparator<Value> = (a: Value, b: Value) => boolean;
|
||||||
|
|
||||||
@@ -19,15 +19,17 @@ export type Comparator<Value> = (a: Value, b: Value) => boolean;
|
|||||||
* @example
|
* @example
|
||||||
* const externalValue = ref(0);
|
* const externalValue = ref(0);
|
||||||
* const cachedValue = useCached(externalValue, (a, b) => a === b, { immediate: true });
|
* const cachedValue = useCached(externalValue, (a, b) => a === b, { immediate: true });
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
*/
|
*/
|
||||||
export function useCached<Value = unknown>(
|
export function useCached<Value = unknown>(
|
||||||
externalValue: Ref<Value>,
|
externalValue: MaybeRefOrGetter<Value>,
|
||||||
comparator: Comparator<Value> = (a, b) => a === b,
|
comparator: Comparator<Value> = (a, b) => a === b,
|
||||||
watchOptions?: WatchOptions,
|
watchOptions?: WatchOptions,
|
||||||
): Ref<Value> {
|
): Ref<Value> {
|
||||||
const cached = ref(externalValue.value) as Ref<Value>;
|
const cached = ref(toValue(externalValue)) as Ref<Value>;
|
||||||
|
|
||||||
watch(() => externalValue.value, (value) => {
|
watch(() => toValue(externalValue), (value) => {
|
||||||
if (!comparator(value, cached.value))
|
if (!comparator(value, cached.value))
|
||||||
cached.value = value;
|
cached.value = value;
|
||||||
}, watchOptions);
|
}, watchOptions);
|
||||||
|
|||||||
60
packages/vue/src/composables/useClamp/index.test.ts
Normal file
60
packages/vue/src/composables/useClamp/index.test.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { ref, readonly, computed } from 'vue';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { useClamp } from '.';
|
||||||
|
|
||||||
|
describe('useClamp', () => {
|
||||||
|
it('non-reactive values should be clamped', () => {
|
||||||
|
const clampedValue = useClamp(10, 0, 5);
|
||||||
|
|
||||||
|
expect(clampedValue.value).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamp the value within the given range', () => {
|
||||||
|
const value = ref(10);
|
||||||
|
const clampedValue = useClamp(value, 0, 5);
|
||||||
|
|
||||||
|
expect(clampedValue.value).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamp the value within the given range using functions', () => {
|
||||||
|
const value = ref(10);
|
||||||
|
const clampedValue = useClamp(value, () => 0, () => 5);
|
||||||
|
|
||||||
|
expect(clampedValue.value).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamp readonly values', () => {
|
||||||
|
const computedValue = computed(() => 10);
|
||||||
|
const readonlyValue = readonly(ref(10));
|
||||||
|
const clampedValue1 = useClamp(computedValue, 0, 5);
|
||||||
|
const clampedValue2 = useClamp(readonlyValue, 0, 5);
|
||||||
|
|
||||||
|
expect(clampedValue1.value).toBe(5);
|
||||||
|
expect(clampedValue2.value).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update the clamped value when the original value changes', () => {
|
||||||
|
const value = ref(10);
|
||||||
|
const clampedValue = useClamp(value, 0, 5);
|
||||||
|
value.value = 3;
|
||||||
|
|
||||||
|
expect(clampedValue.value).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update the clamped value when the min or max changes', () => {
|
||||||
|
const value = ref(10);
|
||||||
|
const min = ref(0);
|
||||||
|
const max = ref(5);
|
||||||
|
const clampedValue = useClamp(value, min, max);
|
||||||
|
|
||||||
|
expect(clampedValue.value).toBe(5);
|
||||||
|
|
||||||
|
max.value = 15;
|
||||||
|
|
||||||
|
expect(clampedValue.value).toBe(10);
|
||||||
|
|
||||||
|
min.value = 11;
|
||||||
|
|
||||||
|
expect(clampedValue.value).toBe(11);
|
||||||
|
});
|
||||||
|
});
|
||||||
39
packages/vue/src/composables/useClamp/index.ts
Normal file
39
packages/vue/src/composables/useClamp/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { clamp, isFunction } from '@robonen/stdlib';
|
||||||
|
import { computed, isReadonly, ref, toValue, type ComputedRef, type MaybeRef, type MaybeRefOrGetter, type WritableComputedRef } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useClamp
|
||||||
|
* @category Math
|
||||||
|
* @description Clamps a value between a minimum and maximum value
|
||||||
|
*
|
||||||
|
* @param {MaybeRefOrGetter<number>} value The value to clamp
|
||||||
|
* @param {MaybeRefOrGetter<number>} min The minimum value
|
||||||
|
* @param {MaybeRefOrGetter<number>} max The maximum value
|
||||||
|
* @returns {ComputedRef<number>} The clamped value
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const value = ref(10);
|
||||||
|
* const clampedValue = useClamp(value, 0, 5);
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const value = ref(10);
|
||||||
|
* const clampedValue = useClamp(value, () => 0, () => 5);
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export function useClamp(value: MaybeRef<number>, min: MaybeRefOrGetter<number>, max: MaybeRefOrGetter<number>): WritableComputedRef<number>;
|
||||||
|
export function useClamp(value: MaybeRefOrGetter<number>, min: MaybeRefOrGetter<number>, max: MaybeRefOrGetter<number>): ComputedRef<number> {
|
||||||
|
if (isFunction(value) || isReadonly(value))
|
||||||
|
return computed(() => clamp(toValue(value), toValue(min), toValue(max)));
|
||||||
|
|
||||||
|
const _value = ref(value);
|
||||||
|
|
||||||
|
return computed<number>({
|
||||||
|
get() {
|
||||||
|
return clamp(_value.value, toValue(min), toValue(max));
|
||||||
|
},
|
||||||
|
set(newValue) {
|
||||||
|
_value.value = clamp(newValue, toValue(min), toValue(max));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
69
packages/vue/src/composables/useContextFactory/index.test.ts
Normal file
69
packages/vue/src/composables/useContextFactory/index.test.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useContextFactory } from '.';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { VueToolsError } from '../../utils';
|
||||||
|
|
||||||
|
function testFactory<Data>(
|
||||||
|
data: Data,
|
||||||
|
options?: { contextName?: string, fallback?: Data },
|
||||||
|
) {
|
||||||
|
const contextName = options?.contextName ?? 'TestContext';
|
||||||
|
|
||||||
|
const [inject, provide] = useContextFactory(contextName);
|
||||||
|
|
||||||
|
const Child = defineComponent({
|
||||||
|
setup() {
|
||||||
|
const value = inject(options?.fallback);
|
||||||
|
return { value };
|
||||||
|
},
|
||||||
|
template: `{{ value }}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Parent = defineComponent({
|
||||||
|
components: { Child },
|
||||||
|
setup() {
|
||||||
|
provide(data);
|
||||||
|
},
|
||||||
|
template: `<Child />`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
Parent,
|
||||||
|
Child,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: maybe replace template with passing mock functions to setup
|
||||||
|
|
||||||
|
describe('useContextFactory', () => {
|
||||||
|
it('provide and inject context correctly', () => {
|
||||||
|
const { Parent } = testFactory('test');
|
||||||
|
|
||||||
|
const component = mount(Parent);
|
||||||
|
|
||||||
|
expect(component.text()).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throw an error when context is not provided', () => {
|
||||||
|
const { Child } = testFactory('test');
|
||||||
|
|
||||||
|
expect(() => mount(Child)).toThrow(VueToolsError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inject a fallback value when context is not provided', () => {
|
||||||
|
const { Child } = testFactory('test', { fallback: 'fallback' });
|
||||||
|
|
||||||
|
const component = mount(Child);
|
||||||
|
|
||||||
|
expect(component.text()).toBe('fallback');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly handle null values', () => {
|
||||||
|
const { Parent } = testFactory(null);
|
||||||
|
|
||||||
|
const component = mount(Parent);
|
||||||
|
|
||||||
|
expect(component.text()).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
36
packages/vue/src/composables/useContextFactory/index.ts
Normal file
36
packages/vue/src/composables/useContextFactory/index.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { inject, provide, type InjectionKey } from 'vue';
|
||||||
|
import { VueToolsError } from '../..';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useContextFactory
|
||||||
|
* @category Utilities
|
||||||
|
* @description A composable that provides a factory for creating context with unique key
|
||||||
|
*
|
||||||
|
* @param {string} name The name of the context
|
||||||
|
* @returns {readonly [injectContext, provideContext]} The context factory
|
||||||
|
* @throws {VueToolsError} when the context is not provided
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [injectContext, provideContext] = useContextFactory('MyContext');
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export function useContextFactory<ContextValue>(name: string) {
|
||||||
|
const injectionKey: InjectionKey<ContextValue> = Symbol(name);
|
||||||
|
|
||||||
|
const injectContext = <Fallback extends ContextValue = ContextValue>(fallback?: Fallback) => {
|
||||||
|
const context = inject(injectionKey, fallback);
|
||||||
|
|
||||||
|
if (context !== undefined)
|
||||||
|
return context;
|
||||||
|
|
||||||
|
throw new VueToolsError(`useContextFactory: '${name}' context is not provided`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const provideContext = (context: ContextValue) => {
|
||||||
|
provide(injectionKey, context);
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
return [injectContext, provideContext] as const;
|
||||||
|
}
|
||||||
@@ -13,6 +13,11 @@ describe('useCounter', () => {
|
|||||||
expect(count.value).toBe(5);
|
expect(count.value).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('initialize count with the provided initial value from a getter', () => {
|
||||||
|
const { count } = useCounter(() => 5);
|
||||||
|
expect(count.value).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
it('increment count by 1 by default', () => {
|
it('increment count by 1 by default', () => {
|
||||||
const { count, increment } = useCounter(0);
|
const { count, increment } = useCounter(0);
|
||||||
increment();
|
increment();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ref, unref, type MaybeRef, type Ref } from 'vue';
|
import { ref, toValue, type MaybeRefOrGetter, type Ref } from 'vue';
|
||||||
import { clamp } from '@robonen/stdlib';
|
import { clamp } from '@robonen/stdlib';
|
||||||
|
|
||||||
export interface UseCounterOptions {
|
export interface UseCounterOptions {
|
||||||
@@ -31,13 +31,15 @@ export interface UseConterReturn {
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const { count, increment, decrement, set, get, reset } = useCounter(0, { min: 0, max: 10 });
|
* const { count, increment, decrement, set, get, reset } = useCounter(0, { min: 0, max: 10 });
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
*/
|
*/
|
||||||
export function useCounter(
|
export function useCounter(
|
||||||
initialValue: MaybeRef<number> = 0,
|
initialValue: MaybeRefOrGetter<number> = 0,
|
||||||
options: UseCounterOptions = {},
|
options: UseCounterOptions = {},
|
||||||
): UseConterReturn {
|
): UseConterReturn {
|
||||||
let _initialValue = unref(initialValue);
|
let _initialValue = toValue(initialValue);
|
||||||
const count = ref(initialValue);
|
const count = ref(_initialValue);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
min = Number.MIN_SAFE_INTEGER,
|
min = Number.MIN_SAFE_INTEGER,
|
||||||
|
|||||||
136
packages/vue/src/composables/useEventListener/index.ts
Normal file
136
packages/vue/src/composables/useEventListener/index.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { isArray, isString, noop, type Arrayable, type VoidFunction } from '@robonen/stdlib';
|
||||||
|
import type { MaybeRefOrGetter } from 'vue';
|
||||||
|
import { defaultWindow } from '../..';
|
||||||
|
|
||||||
|
// TODO: wip
|
||||||
|
|
||||||
|
interface InferEventTarget<Events> {
|
||||||
|
addEventListener: (event: Events, listener?: any, options?: any) => any;
|
||||||
|
removeEventListener: (event: Events, listener?: any, options?: any) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeneralEventListener<E = Event> {
|
||||||
|
(evt: E): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WindowEventName = keyof WindowEventMap;
|
||||||
|
export type DocumentEventName = keyof DocumentEventMap;
|
||||||
|
export type ElementEventName = keyof HTMLElementEventMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useEventListener
|
||||||
|
* @category Elements
|
||||||
|
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
||||||
|
*
|
||||||
|
* Overload 1: Omitted window target
|
||||||
|
*/
|
||||||
|
export function useEventListener<E extends WindowEventName>(
|
||||||
|
event: Arrayable<E>,
|
||||||
|
listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => any>,
|
||||||
|
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
|
||||||
|
): VoidFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useEventListener
|
||||||
|
* @category Elements
|
||||||
|
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
||||||
|
*
|
||||||
|
* Overload 2: Explicit window target
|
||||||
|
*/
|
||||||
|
export function useEventListener<E extends WindowEventName>(
|
||||||
|
target: Window,
|
||||||
|
event: Arrayable<E>,
|
||||||
|
listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => any>,
|
||||||
|
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
|
||||||
|
): VoidFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useEventListener
|
||||||
|
* @category Elements
|
||||||
|
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
||||||
|
*
|
||||||
|
* Overload 3: Explicit document target
|
||||||
|
*/
|
||||||
|
export function useEventListener<E extends DocumentEventName>(
|
||||||
|
target: Document,
|
||||||
|
event: Arrayable<E>,
|
||||||
|
listener: Arrayable<(this: Document, ev: DocumentEventMap[E]) => any>,
|
||||||
|
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
|
||||||
|
): VoidFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useEventListener
|
||||||
|
* @category Elements
|
||||||
|
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
||||||
|
*
|
||||||
|
* Overload 4: Explicit HTMLElement target
|
||||||
|
*/
|
||||||
|
export function useEventListener<E extends ElementEventName>(
|
||||||
|
target: MaybeRefOrGetter<HTMLElement | null | undefined>,
|
||||||
|
event: Arrayable<E>,
|
||||||
|
listener: Arrayable<(this: HTMLElement, ev: HTMLElementEventMap[E]) => any>,
|
||||||
|
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
|
||||||
|
): VoidFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useEventListener
|
||||||
|
* @category Elements
|
||||||
|
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
||||||
|
*
|
||||||
|
* Overload 5: Custom target with inferred event type
|
||||||
|
*/
|
||||||
|
export function useEventListener<Names extends string, EventType = Event>(
|
||||||
|
target: MaybeRefOrGetter<InferEventTarget<Names> | null | undefined>,
|
||||||
|
event: Arrayable<Names>,
|
||||||
|
listener: Arrayable<GeneralEventListener<EventType>>,
|
||||||
|
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useEventListener
|
||||||
|
* @category Elements
|
||||||
|
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
||||||
|
*
|
||||||
|
* Overload 6: Custom event target fallback
|
||||||
|
*/
|
||||||
|
export function useEventListener<EventType = Event>(
|
||||||
|
target: MaybeRefOrGetter<EventTarget | null | undefined>,
|
||||||
|
event: Arrayable<string>,
|
||||||
|
listener: Arrayable<GeneralEventListener<EventType>>,
|
||||||
|
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
|
||||||
|
): VoidFunction;
|
||||||
|
|
||||||
|
export function useEventListener(...args: any[]) {
|
||||||
|
let target: MaybeRefOrGetter<EventTarget> | undefined;
|
||||||
|
let events: Arrayable<string>;
|
||||||
|
let listeners: Arrayable<Function>;
|
||||||
|
let options: MaybeRefOrGetter<boolean | AddEventListenerOptions> | undefined;
|
||||||
|
|
||||||
|
if (isString(args[0]) || isArray(args[0])) {
|
||||||
|
[events, listeners, options] = args;
|
||||||
|
target = defaultWindow;
|
||||||
|
} else {
|
||||||
|
[target, events, listeners, options] = args;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target)
|
||||||
|
return noop;
|
||||||
|
|
||||||
|
if (!isArray(events))
|
||||||
|
events = [events];
|
||||||
|
|
||||||
|
if (!isArray(listeners))
|
||||||
|
listeners = [listeners];
|
||||||
|
|
||||||
|
const cleanups: Function[] = [];
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
cleanups.forEach(fn => fn());
|
||||||
|
cleanups.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const register = (el: any, event: string, listener: any, options: any) => {
|
||||||
|
el.addEventListener(event, listener, options);
|
||||||
|
return () => el.removeEventListener(event, listener, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
packages/vue/src/composables/useLastChanged/index.test.ts
Normal file
50
packages/vue/src/composables/useLastChanged/index.test.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { ref, nextTick } from 'vue';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { useLastChanged } from '.';
|
||||||
|
import { timestamp } from '@robonen/stdlib';
|
||||||
|
|
||||||
|
describe('useLastChanged', () => {
|
||||||
|
it('initialize with null if no initialValue is provided', () => {
|
||||||
|
const source = ref(0);
|
||||||
|
const lastChanged = useLastChanged(source);
|
||||||
|
|
||||||
|
expect(lastChanged.value).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initialize with the provided initialValue', () => {
|
||||||
|
const source = ref(0);
|
||||||
|
const initialValue = 123456789;
|
||||||
|
const lastChanged = useLastChanged(source, { initialValue });
|
||||||
|
|
||||||
|
expect(lastChanged.value).toBe(initialValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update the timestamp when the source changes', async () => {
|
||||||
|
const source = ref(0);
|
||||||
|
const lastChanged = useLastChanged(source);
|
||||||
|
|
||||||
|
const initialTimestamp = lastChanged.value;
|
||||||
|
source.value = 1;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(lastChanged.value).not.toBe(initialTimestamp);
|
||||||
|
expect(lastChanged.value).toBeLessThanOrEqual(timestamp());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update the timestamp immediately if immediate option is true', async () => {
|
||||||
|
const source = ref(0);
|
||||||
|
const lastChanged = useLastChanged(source, { immediate: true });
|
||||||
|
|
||||||
|
expect(lastChanged.value).toBeLessThanOrEqual(timestamp());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not update the timestamp if the source does not change', async () => {
|
||||||
|
const source = ref(0);
|
||||||
|
const lastChanged = useLastChanged(source);
|
||||||
|
|
||||||
|
const initialTimestamp = lastChanged.value;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(lastChanged.value).toBe(initialTimestamp);
|
||||||
|
});
|
||||||
|
});
|
||||||
38
packages/vue/src/composables/useLastChanged/index.ts
Normal file
38
packages/vue/src/composables/useLastChanged/index.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { timestamp } from '@robonen/stdlib';
|
||||||
|
import { ref, watch, type WatchSource, type WatchOptions, type Ref } from 'vue';
|
||||||
|
|
||||||
|
export interface UseLastChangedOptions<
|
||||||
|
Immediate extends boolean,
|
||||||
|
InitialValue extends number | null | undefined = undefined,
|
||||||
|
> extends WatchOptions<Immediate> {
|
||||||
|
initialValue?: InitialValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useLastChanged
|
||||||
|
* @category State
|
||||||
|
* @description Records the last time a value changed
|
||||||
|
*
|
||||||
|
* @param {WatchSource} source The value to track
|
||||||
|
* @param {UseLastChangedOptions} [options={}] The options for the last changed tracker
|
||||||
|
* @returns {Ref<number | null>} The timestamp of the last change
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const value = ref(0);
|
||||||
|
* const lastChanged = useLastChanged(value);
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const value = ref(0);
|
||||||
|
* const lastChanged = useLastChanged(value, { immediate: true });
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export function useLastChanged(source: WatchSource, options?: UseLastChangedOptions<false>): Ref<number | null>;
|
||||||
|
export function useLastChanged(source: WatchSource, options: UseLastChangedOptions<true> | UseLastChangedOptions<boolean, number>): Ref<number>
|
||||||
|
export function useLastChanged(source: WatchSource, options: UseLastChangedOptions<boolean, any> = {}): Ref<number | null> | Ref<number> {
|
||||||
|
const lastChanged = ref<number | null>(options.initialValue ?? null);
|
||||||
|
|
||||||
|
watch(source, () => lastChanged.value = timestamp(), options);
|
||||||
|
|
||||||
|
return lastChanged;
|
||||||
|
}
|
||||||
27
packages/vue/src/composables/useMounted/index.test.ts
Normal file
27
packages/vue/src/composables/useMounted/index.test.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { defineComponent, nextTick, ref } from 'vue';
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { useMounted } from '.';
|
||||||
|
|
||||||
|
const ComponentStub = defineComponent({
|
||||||
|
setup() {
|
||||||
|
const isMounted = useMounted();
|
||||||
|
|
||||||
|
return { isMounted };
|
||||||
|
},
|
||||||
|
template: `<div>{{ isMounted }}</div>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useMounted', () => {
|
||||||
|
it('return the mounted state of the component', async () => {
|
||||||
|
const component = mount(ComponentStub);
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
expect(component.text()).toBe('false');
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// Will trigger a render
|
||||||
|
expect(component.text()).toBe('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
27
packages/vue/src/composables/useMounted/index.ts
Normal file
27
packages/vue/src/composables/useMounted/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { onMounted, readonly, ref, type ComponentInternalInstance } from 'vue';
|
||||||
|
import { getLifeCycleTarger } from '../..';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useMounted
|
||||||
|
* @category Components
|
||||||
|
* @description Returns a ref that tracks the mounted state of the component (doesn't track the unmounted state)
|
||||||
|
*
|
||||||
|
* @param {ComponentInternalInstance} [instance] The component instance to track the mounted state for
|
||||||
|
* @returns {Readonly<Ref<boolean>>} The mounted state of the component
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const isMounted = useMounted();
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const isMounted = useMounted(getCurrentInstance());
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export function useMounted(instance?: ComponentInternalInstance) {
|
||||||
|
const isMounted = ref(false);
|
||||||
|
const targetInstance = getLifeCycleTarger(instance);
|
||||||
|
|
||||||
|
onMounted(() => isMounted.value = true, targetInstance);
|
||||||
|
|
||||||
|
return readonly(isMounted);
|
||||||
|
}
|
||||||
147
packages/vue/src/composables/useOffsetPagination/index.test.ts
Normal file
147
packages/vue/src/composables/useOffsetPagination/index.test.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { nextTick, ref } from 'vue';
|
||||||
|
import { useOffsetPagination } from '.';
|
||||||
|
|
||||||
|
describe('useOffsetPagination', () => {
|
||||||
|
it('initialize with default values without options', () => {
|
||||||
|
const { currentPage, currentPageSize, totalPages, isFirstPage } = useOffsetPagination({});
|
||||||
|
|
||||||
|
expect(currentPage.value).toBe(1);
|
||||||
|
expect(currentPageSize.value).toBe(10);
|
||||||
|
expect(totalPages.value).toBe(Infinity);
|
||||||
|
expect(isFirstPage.value).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calculate total pages correctly', () => {
|
||||||
|
const { totalPages } = useOffsetPagination({ total: 100, pageSize: 10 });
|
||||||
|
|
||||||
|
expect(totalPages.value).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update current page correctly', () => {
|
||||||
|
const { currentPage, next, previous, select } = useOffsetPagination({ total: 100, pageSize: 10 });
|
||||||
|
|
||||||
|
next();
|
||||||
|
expect(currentPage.value).toBe(2);
|
||||||
|
|
||||||
|
previous();
|
||||||
|
expect(currentPage.value).toBe(1);
|
||||||
|
|
||||||
|
select(5);
|
||||||
|
expect(currentPage.value).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle out of bounds increments correctly', () => {
|
||||||
|
const { currentPage, next, previous } = useOffsetPagination({ total: 10, pageSize: 5 });
|
||||||
|
|
||||||
|
next();
|
||||||
|
next();
|
||||||
|
next();
|
||||||
|
|
||||||
|
expect(currentPage.value).toBe(2);
|
||||||
|
|
||||||
|
previous();
|
||||||
|
previous();
|
||||||
|
previous();
|
||||||
|
|
||||||
|
expect(currentPage.value).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle page boundaries correctly', () => {
|
||||||
|
const { currentPage, isFirstPage, isLastPage } = useOffsetPagination({ total: 20, pageSize: 10 });
|
||||||
|
|
||||||
|
expect(currentPage.value).toBe(1);
|
||||||
|
expect(isFirstPage.value).toBe(true);
|
||||||
|
expect(isLastPage.value).toBe(false);
|
||||||
|
|
||||||
|
currentPage.value = 2;
|
||||||
|
|
||||||
|
expect(currentPage.value).toBe(2);
|
||||||
|
expect(isFirstPage.value).toBe(false);
|
||||||
|
expect(isLastPage.value).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('call onPageChange callback', async () => {
|
||||||
|
const onPageChange = vi.fn();
|
||||||
|
const { currentPage, next } = useOffsetPagination({ total: 100, pageSize: 10, onPageChange });
|
||||||
|
|
||||||
|
next();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(onPageChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onPageChange).toHaveBeenCalledWith(expect.objectContaining({ currentPage: currentPage.value }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('call onPageSizeChange callback', async () => {
|
||||||
|
const onPageSizeChange = vi.fn();
|
||||||
|
const pageSize = ref(10);
|
||||||
|
const { currentPageSize } = useOffsetPagination({ total: 100, pageSize, onPageSizeChange });
|
||||||
|
|
||||||
|
pageSize.value = 20;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(onPageSizeChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onPageSizeChange).toHaveBeenCalledWith(expect.objectContaining({ currentPageSize: currentPageSize.value }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('call onPageCountChange callback', async () => {
|
||||||
|
const onTotalPagesChange = vi.fn();
|
||||||
|
const total = ref(100);
|
||||||
|
const { totalPages } = useOffsetPagination({ total, pageSize: 10, onTotalPagesChange });
|
||||||
|
|
||||||
|
total.value = 200;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(onTotalPagesChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onTotalPagesChange).toHaveBeenCalledWith(expect.objectContaining({ totalPages: totalPages.value }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle complex reactive options', async () => {
|
||||||
|
const total = ref(100);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
const page = ref(1);
|
||||||
|
|
||||||
|
const onPageChange = vi.fn();
|
||||||
|
const onPageSizeChange = vi.fn();
|
||||||
|
const onTotalPagesChange = vi.fn();
|
||||||
|
|
||||||
|
const { currentPage, currentPageSize, totalPages } = useOffsetPagination({
|
||||||
|
total,
|
||||||
|
pageSize,
|
||||||
|
page,
|
||||||
|
onPageChange,
|
||||||
|
onPageSizeChange,
|
||||||
|
onTotalPagesChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial values
|
||||||
|
expect(currentPage.value).toBe(1);
|
||||||
|
expect(currentPageSize.value).toBe(10);
|
||||||
|
expect(totalPages.value).toBe(10);
|
||||||
|
expect(onPageChange).toHaveBeenCalledTimes(0);
|
||||||
|
expect(onPageSizeChange).toHaveBeenCalledTimes(0);
|
||||||
|
expect(onTotalPagesChange).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
total.value = 300;
|
||||||
|
pageSize.value = 15;
|
||||||
|
page.value = 2;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// Valid values after changes
|
||||||
|
expect(currentPage.value).toBe(2);
|
||||||
|
expect(currentPageSize.value).toBe(15);
|
||||||
|
expect(totalPages.value).toBe(20);
|
||||||
|
expect(onPageChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onPageSizeChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onTotalPagesChange).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
page.value = 21;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// Invalid values after changes
|
||||||
|
expect(currentPage.value).toBe(20);
|
||||||
|
expect(onPageChange).toHaveBeenCalledTimes(2);
|
||||||
|
expect(onPageSizeChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onTotalPagesChange).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
126
packages/vue/src/composables/useOffsetPagination/index.ts
Normal file
126
packages/vue/src/composables/useOffsetPagination/index.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import type { VoidFunction } from '@robonen/stdlib';
|
||||||
|
import { computed, reactive, toValue, watch, type ComputedRef, type MaybeRef, type MaybeRefOrGetter, type UnwrapNestedRefs, type WritableComputedRef } from 'vue';
|
||||||
|
import { useClamp } from '../useClamp';
|
||||||
|
|
||||||
|
// TODO: sync returned refs with passed refs
|
||||||
|
|
||||||
|
export interface UseOffsetPaginationOptions {
|
||||||
|
total?: MaybeRefOrGetter<number>;
|
||||||
|
pageSize?: MaybeRef<number>;
|
||||||
|
page?: MaybeRef<number>;
|
||||||
|
onPageChange?: (returnValue: UnwrapNestedRefs<UseOffsetPaginationReturn>) => unknown;
|
||||||
|
onPageSizeChange?: (returnValue: UnwrapNestedRefs<UseOffsetPaginationReturn>) => unknown;
|
||||||
|
onTotalPagesChange?: (returnValue: UnwrapNestedRefs<UseOffsetPaginationReturn>) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseOffsetPaginationReturn {
|
||||||
|
currentPage: WritableComputedRef<number>;
|
||||||
|
currentPageSize: WritableComputedRef<number>;
|
||||||
|
totalPages: ComputedRef<number>;
|
||||||
|
isFirstPage: ComputedRef<boolean>;
|
||||||
|
isLastPage: ComputedRef<boolean>;
|
||||||
|
next: VoidFunction;
|
||||||
|
previous: VoidFunction;
|
||||||
|
select: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseOffsetPaginationInfinityReturn = Omit<UseOffsetPaginationReturn, 'isLastPage'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useOffsetPagination
|
||||||
|
* @category Utilities
|
||||||
|
* @description A composable function that provides pagination functionality for offset based pagination
|
||||||
|
*
|
||||||
|
* @param {UseOffsetPaginationOptions} options The options for the pagination
|
||||||
|
* @param {MaybeRefOrGetter<number>} options.total The total number of items
|
||||||
|
* @param {MaybeRef<number>} options.pageSize The number of items per page
|
||||||
|
* @param {MaybeRef<number>} options.page The current page
|
||||||
|
* @param {(returnValue: UnwrapNestedRefs<UseOffsetPaginationReturn>) => unknown} options.onPageChange A callback that is called when the page changes
|
||||||
|
* @param {(returnValue: UnwrapNestedRefs<UseOffsetPaginationReturn>) => unknown} options.onPageSizeChange A callback that is called when the page size changes
|
||||||
|
* @param {(returnValue: UnwrapNestedRefs<UseOffsetPaginationReturn>) => unknown} options.onTotalPagesChange A callback that is called when the total number of pages changes
|
||||||
|
* @returns {UseOffsetPaginationReturn} The pagination object
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const {
|
||||||
|
* currentPage,
|
||||||
|
* currentPageSize,
|
||||||
|
* totalPages,
|
||||||
|
* isFirstPage,
|
||||||
|
* isLastPage,
|
||||||
|
* next,
|
||||||
|
* previous,
|
||||||
|
* select,
|
||||||
|
* } = useOffsetPagination({ total: 100, pageSize: 10, page: 1 });
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const {
|
||||||
|
* currentPage,
|
||||||
|
* } = useOffsetPagination({
|
||||||
|
* total: 100,
|
||||||
|
* pageSize: 10,
|
||||||
|
* page: 1,
|
||||||
|
* onPageChange: ({ currentPage }) => console.log(currentPage),
|
||||||
|
* onPageSizeChange: ({ currentPageSize }) => console.log(currentPageSize),
|
||||||
|
* onTotalPagesChange: ({ totalPages }) => console.log(totalPages),
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export function useOffsetPagination(options: Omit<UseOffsetPaginationOptions, 'total'>): UseOffsetPaginationInfinityReturn;
|
||||||
|
export function useOffsetPagination(options: UseOffsetPaginationOptions): UseOffsetPaginationReturn;
|
||||||
|
export function useOffsetPagination(options: UseOffsetPaginationOptions): UseOffsetPaginationReturn {
|
||||||
|
const {
|
||||||
|
total = Number.POSITIVE_INFINITY,
|
||||||
|
pageSize = 10,
|
||||||
|
page = 1,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const currentPageSize = useClamp(pageSize, 1, Number.POSITIVE_INFINITY);
|
||||||
|
|
||||||
|
const totalPages = computed(() => Math.max(
|
||||||
|
1,
|
||||||
|
Math.ceil(toValue(total) / toValue(currentPageSize))
|
||||||
|
));
|
||||||
|
|
||||||
|
const currentPage = useClamp(page, 1, totalPages);
|
||||||
|
|
||||||
|
const isFirstPage = computed(() => currentPage.value === 1);
|
||||||
|
const isLastPage = computed(() => currentPage.value === totalPages.value);
|
||||||
|
|
||||||
|
const next = () => currentPage.value++;
|
||||||
|
const previous = () => currentPage.value--;
|
||||||
|
const select = (page: number) => currentPage.value = page;
|
||||||
|
|
||||||
|
const returnValue = {
|
||||||
|
currentPage,
|
||||||
|
currentPageSize,
|
||||||
|
totalPages,
|
||||||
|
isFirstPage,
|
||||||
|
isLastPage,
|
||||||
|
next,
|
||||||
|
previous,
|
||||||
|
select,
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTE: Don't forget to await nextTick() after calling next() or previous() to ensure the callback is called
|
||||||
|
|
||||||
|
if (options.onPageChange) {
|
||||||
|
watch(currentPage, () => {
|
||||||
|
options.onPageChange!(reactive(returnValue));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.onPageSizeChange) {
|
||||||
|
watch(currentPageSize, () => {
|
||||||
|
options.onPageSizeChange!(reactive(returnValue));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.onTotalPagesChange) {
|
||||||
|
watch(totalPages, () => {
|
||||||
|
options.onTotalPagesChange!(reactive(returnValue));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
@@ -19,20 +19,20 @@ describe('useRenderCount', () => {
|
|||||||
const component = mount(ComponentStub);
|
const component = mount(ComponentStub);
|
||||||
|
|
||||||
// Initial render
|
// Initial render
|
||||||
expect(component.vm.count).toBe(0);
|
expect(component.vm.count).toBe(1);
|
||||||
|
|
||||||
component.vm.hiddenCount = 1;
|
component.vm.hiddenCount = 1;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// Will not trigger a render
|
// Will not trigger a render
|
||||||
expect(component.vm.count).toBe(0);
|
expect(component.vm.count).toBe(1);
|
||||||
expect(component.text()).toBe('0');
|
expect(component.text()).toBe('0');
|
||||||
|
|
||||||
component.vm.visibleCount++;
|
component.vm.visibleCount++;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// Will trigger a render
|
// Will trigger a render
|
||||||
expect(component.vm.count).toBe(1);
|
expect(component.vm.count).toBe(2);
|
||||||
expect(component.text()).toBe('1');
|
expect(component.text()).toBe('1');
|
||||||
|
|
||||||
component.vm.visibleCount++;
|
component.vm.visibleCount++;
|
||||||
@@ -40,7 +40,7 @@ describe('useRenderCount', () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// Will trigger a single render for both updates
|
// Will trigger a single render for both updates
|
||||||
expect(component.vm.count).toBe(2);
|
expect(component.vm.count).toBe(3);
|
||||||
expect(component.text()).toBe('3');
|
expect(component.text()).toBe('3');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ describe('useRenderCount', () => {
|
|||||||
|
|
||||||
const count = useRenderCount(instance);
|
const count = useRenderCount(instance);
|
||||||
|
|
||||||
// Initial render
|
// Initial render (should be zero because the component has already rendered on mount)
|
||||||
expect(count.value).toBe(0);
|
expect(count.value).toBe(0);
|
||||||
|
|
||||||
component.vm.hiddenCount = 1;
|
component.vm.hiddenCount = 1;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { onUpdated, readonly, type ComponentInternalInstance } from 'vue';
|
import { onMounted, onUpdated, readonly, type ComponentInternalInstance } from 'vue';
|
||||||
import { useCounter } from '../useCounter';
|
import { useCounter } from '../useCounter';
|
||||||
import { getLifeCycleTarger } from '../../utils';
|
import { getLifeCycleTarger } from '../..';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useRenderCount
|
* @name useRenderCount
|
||||||
@@ -15,11 +15,15 @@ import { getLifeCycleTarger } from '../../utils';
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const count = useRenderCount(getCurrentInstance());
|
* const count = useRenderCount(getCurrentInstance());
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
*/
|
*/
|
||||||
export function useRenderCount(instance?: ComponentInternalInstance) {
|
export function useRenderCount(instance?: ComponentInternalInstance) {
|
||||||
const { count, increment } = useCounter(0);
|
const { count, increment } = useCounter(0);
|
||||||
|
const target = getLifeCycleTarger(instance);
|
||||||
|
|
||||||
onUpdated(increment, getLifeCycleTarger(instance));
|
onMounted(increment, target);
|
||||||
|
onUpdated(increment, target);
|
||||||
|
|
||||||
return readonly(count);
|
return readonly(count);
|
||||||
}
|
}
|
||||||
100
packages/vue/src/composables/useRenderInfo/index.test.ts
Normal file
100
packages/vue/src/composables/useRenderInfo/index.test.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { useRenderInfo } from '.';
|
||||||
|
import { defineComponent, nextTick, ref } from 'vue';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
const NamedComponentStub = defineComponent({
|
||||||
|
name: 'ComponentStub',
|
||||||
|
setup() {
|
||||||
|
const info = useRenderInfo();
|
||||||
|
const visibleCount = ref(0);
|
||||||
|
const hiddenCount = ref(0);
|
||||||
|
|
||||||
|
return { info, visibleCount, hiddenCount };
|
||||||
|
},
|
||||||
|
template: `<div>{{ visibleCount }}</div>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const UnnamedComponentStub = defineComponent({
|
||||||
|
setup() {
|
||||||
|
const info = useRenderInfo();
|
||||||
|
const visibleCount = ref(0);
|
||||||
|
const hiddenCount = ref(0);
|
||||||
|
|
||||||
|
return { info, visibleCount, hiddenCount };
|
||||||
|
},
|
||||||
|
template: `<div>{{ visibleCount }}</div>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useRenderInfo', () => {
|
||||||
|
it('return uid if component name is not available', async () => {
|
||||||
|
const wrapper = mount(UnnamedComponentStub);
|
||||||
|
|
||||||
|
expect(wrapper.vm.info.component).toBe(wrapper.vm.$.uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return render info for the given instance', async () => {
|
||||||
|
const wrapper = mount(NamedComponentStub);
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
expect(wrapper.vm.info.component).toBe('ComponentStub');
|
||||||
|
expect(wrapper.vm.info.count.value).toBe(1);
|
||||||
|
expect(wrapper.vm.info.duration.value).toBeGreaterThan(0);
|
||||||
|
expect(wrapper.vm.info.lastRendered).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
let lastRendered = wrapper.vm.info.lastRendered;
|
||||||
|
let duration = wrapper.vm.info.duration.value;
|
||||||
|
|
||||||
|
// Will not trigger a render
|
||||||
|
wrapper.vm.hiddenCount++;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.info.component).toBe('ComponentStub');
|
||||||
|
expect(wrapper.vm.info.count.value).toBe(1);
|
||||||
|
expect(wrapper.vm.info.duration.value).toBe(duration);
|
||||||
|
expect(wrapper.vm.info.lastRendered).toBe(lastRendered);
|
||||||
|
|
||||||
|
// Will trigger a render
|
||||||
|
wrapper.vm.visibleCount++;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.info.component).toBe('ComponentStub');
|
||||||
|
expect(wrapper.vm.info.count.value).toBe(2);
|
||||||
|
expect(wrapper.vm.info.duration.value).not.toBe(duration);
|
||||||
|
expect(wrapper.vm.info.lastRendered).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be used with a specific component instance', async () => {
|
||||||
|
const wrapper = mount(NamedComponentStub);
|
||||||
|
const instance = wrapper.vm.$;
|
||||||
|
|
||||||
|
const info = useRenderInfo(instance);
|
||||||
|
|
||||||
|
// Initial render (should be zero because the component has already rendered on mount)
|
||||||
|
expect(info.component).toBe('ComponentStub');
|
||||||
|
expect(info.count.value).toBe(0);
|
||||||
|
expect(info.duration.value).toBe(0);
|
||||||
|
expect(info.lastRendered).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
let lastRendered = info.lastRendered;
|
||||||
|
let duration = info.duration.value;
|
||||||
|
|
||||||
|
// Will not trigger a render
|
||||||
|
wrapper.vm.hiddenCount++;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(info.component).toBe('ComponentStub');
|
||||||
|
expect(info.count.value).toBe(0);
|
||||||
|
expect(info.duration.value).toBe(duration);
|
||||||
|
expect(info.lastRendered).toBe(lastRendered);
|
||||||
|
|
||||||
|
// Will trigger a render
|
||||||
|
wrapper.vm.visibleCount++;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(info.component).toBe('ComponentStub');
|
||||||
|
expect(info.count.value).toBe(1);
|
||||||
|
expect(info.duration.value).not.toBe(duration);
|
||||||
|
expect(info.lastRendered).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
41
packages/vue/src/composables/useRenderInfo/index.ts
Normal file
41
packages/vue/src/composables/useRenderInfo/index.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { timestamp } from '@robonen/stdlib';
|
||||||
|
import { onBeforeMount, onBeforeUpdate, onMounted, onUpdated, readonly, ref, type ComponentInternalInstance } from 'vue';
|
||||||
|
import { useRenderCount } from '../useRenderCount';
|
||||||
|
import { getLifeCycleTarger } from '../..';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useRenderInfo
|
||||||
|
* @category Components
|
||||||
|
* @description Returns information about the component's render count and the last time it was rendered
|
||||||
|
*
|
||||||
|
* @param {ComponentInternalInstance} [instance] The component instance to track the render count for
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { component, count, duration, lastRendered } = useRenderInfo();
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { component, count, duration, lastRendered } = useRenderInfo(getCurrentInstance());
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export function useRenderInfo(instance?: ComponentInternalInstance) {
|
||||||
|
const target = getLifeCycleTarger(instance);
|
||||||
|
const duration = ref(0);
|
||||||
|
|
||||||
|
const startMark = () => duration.value = performance.now();
|
||||||
|
const endMark = () => duration.value = Math.max(performance.now() - duration.value, 0);
|
||||||
|
|
||||||
|
onBeforeMount(startMark, target);
|
||||||
|
onMounted(endMark, target);
|
||||||
|
|
||||||
|
onBeforeUpdate(startMark, target);
|
||||||
|
onUpdated(endMark, target);
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: target?.type.name ?? target?.uid,
|
||||||
|
count: useRenderCount(instance),
|
||||||
|
duration: readonly(duration),
|
||||||
|
lastRendered: timestamp(),
|
||||||
|
};
|
||||||
|
}
|
||||||
37
packages/vue/src/composables/useSupported/index.test.ts
Normal file
37
packages/vue/src/composables/useSupported/index.test.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { useSupported } from '.';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
const ComponentStub = defineComponent({
|
||||||
|
props: {
|
||||||
|
location: {
|
||||||
|
type: String,
|
||||||
|
default: 'location',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const isSupported = useSupported(() => props.location in window);
|
||||||
|
|
||||||
|
return { isSupported };
|
||||||
|
},
|
||||||
|
template: `<div>{{ isSupported }}</div>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useSupported', () => {
|
||||||
|
it('return whether the feature is supported', async () => {
|
||||||
|
const component = mount(ComponentStub);
|
||||||
|
|
||||||
|
expect(component.text()).toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return whether the feature is not supported', async () => {
|
||||||
|
const component = mount(ComponentStub, {
|
||||||
|
props: {
|
||||||
|
location: 'unsupported',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(component.text()).toBe('false');
|
||||||
|
});
|
||||||
|
});
|
||||||
29
packages/vue/src/composables/useSupported/index.ts
Normal file
29
packages/vue/src/composables/useSupported/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
import { useMounted } from '../useMounted';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name useSupported
|
||||||
|
* @category Utilities
|
||||||
|
* @description SSR-friendly way to check if a feature is supported
|
||||||
|
*
|
||||||
|
* @param {Function} feature The feature to check for support
|
||||||
|
* @returns {ComputedRef<boolean>} Whether the feature is supported
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const isSupported = useSupported(() => 'IntersectionObserver' in window);
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const isSupported = useSupported(() => 'ResizeObserver' in window);
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export function useSupported(feature: () => unknown) {
|
||||||
|
const isMounted = useMounted();
|
||||||
|
|
||||||
|
return computed(() => {
|
||||||
|
// add reactive dependency on isMounted
|
||||||
|
isMounted.value;
|
||||||
|
|
||||||
|
return Boolean(feature());
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ import { isArray } from '@robonen/stdlib';
|
|||||||
* const source = ref(0);
|
* const source = ref(0);
|
||||||
* const target1 = ref(0);
|
* const target1 = ref(0);
|
||||||
* useSyncRefs(source, target1, { immediate: true });
|
* useSyncRefs(source, target1, { immediate: true });
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
*/
|
*/
|
||||||
export function useSyncRefs<T = unknown>(
|
export function useSyncRefs<T = unknown>(
|
||||||
source: WatchSource<T>,
|
source: WatchSource<T>,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { isRef, ref, toValue, type MaybeRefOrGetter, type MaybeRef, type Ref } from 'vue';
|
import { isRef, ref, toValue, type MaybeRefOrGetter, type MaybeRef, type Ref } from 'vue';
|
||||||
|
|
||||||
|
// TODO: wip
|
||||||
|
|
||||||
export interface UseToggleOptions<Enabled, Disabled> {
|
export interface UseToggleOptions<Enabled, Disabled> {
|
||||||
enabledValue?: MaybeRefOrGetter<Enabled>,
|
enabledValue?: MaybeRefOrGetter<Enabled>,
|
||||||
disabledValue?: MaybeRefOrGetter<Disabled>,
|
disabledValue?: MaybeRefOrGetter<Disabled>,
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './composables';
|
export * from './composables';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
export * from './types';
|
||||||
2
packages/vue/src/types/index.ts
Normal file
2
packages/vue/src/types/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './resumable';
|
||||||
|
export * from './window';
|
||||||
30
packages/vue/src/types/resumable.ts
Normal file
30
packages/vue/src/types/resumable.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Often times, we want to pause and resume a process. This is a common pattern in
|
||||||
|
* reactive programming. This interface defines the options and actions for a resumable
|
||||||
|
* process.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options for a resumable process.
|
||||||
|
*
|
||||||
|
* @typedef {Object} ResumableOptions
|
||||||
|
* @property {boolean} [immediate] Whether to immediately resume the process
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export interface ResumableOptions {
|
||||||
|
immediate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actions for a resumable process.
|
||||||
|
*
|
||||||
|
* @typedef {Object} ResumableActions
|
||||||
|
* @property {Function} resume Resumes the process
|
||||||
|
* @property {Function} pause Pauses the process
|
||||||
|
* @property {Function} toggle Toggles the process
|
||||||
|
*/
|
||||||
|
export interface ResumableActions {
|
||||||
|
resume: () => void;
|
||||||
|
pause: () => void;
|
||||||
|
toggle: () => void;
|
||||||
|
}
|
||||||
3
packages/vue/src/types/window.ts
Normal file
3
packages/vue/src/types/window.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { isClient } from '@robonen/platform';
|
||||||
|
|
||||||
|
export const defaultWindow = /* #__PURE__ */ isClient ? window : undefined
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
import { getCurrentInstance, type ComponentInternalInstance } from 'vue';
|
import { getCurrentInstance, type ComponentInternalInstance } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name getLifeCycleTarger
|
||||||
|
* @category Utils
|
||||||
|
* @description Function to get the target instance of the lifecycle hook
|
||||||
|
*
|
||||||
|
* @param {ComponentInternalInstance} target The target instance of the lifecycle hook
|
||||||
|
* @returns {ComponentInternalInstance | null} Instance of the lifecycle hook or null
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const target = getLifeCycleTarger();
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const target = getLifeCycleTarger(instance);
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
export function getLifeCycleTarger(target?: ComponentInternalInstance) {
|
export function getLifeCycleTarger(target?: ComponentInternalInstance) {
|
||||||
return target || getCurrentInstance();
|
return target || getCurrentInstance();
|
||||||
}
|
}
|
||||||
13
packages/vue/src/utils/error.ts
Normal file
13
packages/vue/src/utils/error.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* @name VueToolsError
|
||||||
|
* @category Error
|
||||||
|
* @description VueToolsError is a custom error class that represents an error in Vue Tools
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export class VueToolsError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'VueToolsError';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from './components';
|
export * from './components';
|
||||||
|
export * from './error';
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "@robonen/tsconfig/tsconfig.json"
|
"extends": "@robonen/tsconfig/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["DOM"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
2144
pnpm-lock.yaml
generated
2144
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,10 @@ packages:
|
|||||||
- apps/*
|
- apps/*
|
||||||
- packages/*
|
- packages/*
|
||||||
catalog:
|
catalog:
|
||||||
'@vitest/coverage-v8': ^2.1.2
|
'@vitest/coverage-v8': ^2.1.3
|
||||||
'@vue/test-utils': ^2.4.6
|
'@vue/test-utils': ^2.4.6
|
||||||
jsdom: ^25.0.1
|
jsdom: ^25.0.1
|
||||||
pathe: ^1.1.2
|
pathe: ^1.1.2
|
||||||
unbuild: 3.0.0-rc.8
|
unbuild: 3.0.0-rc.11
|
||||||
vitest: ^2.1.2
|
vitest: ^2.1.3
|
||||||
vue: ^3.5.11
|
vue: ^3.5.12
|
||||||
|
|||||||
Reference in New Issue
Block a user