Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/mrdulin/dm-utils
A dozen of utils for Front-End Development
https://github.com/mrdulin/dm-utils
clipboard frontend reactjs utils utils-library
Last synced: about 2 months ago
JSON representation
A dozen of utils for Front-End Development
- Host: GitHub
- URL: https://github.com/mrdulin/dm-utils
- Owner: mrdulin
- License: mit
- Created: 2024-03-08T10:29:08.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2024-11-06T03:53:16.000Z (about 2 months ago)
- Last Synced: 2024-11-06T04:31:31.311Z (about 2 months ago)
- Topics: clipboard, frontend, reactjs, utils, utils-library
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/@d-matrix/utils
- Size: 1.54 MB
- Stars: 0
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: readme.md
- License: LICENSE
Awesome Lists containing this project
README
# @d-matrix/utils
![NPM Downloads](https://img.shields.io/npm/dw/%40d-matrix%2Futils)
![npm bundle size](https://img.shields.io/bundlephobia/min/%40d-matrix%2Futils)A dozen of utils for Front-End Development
## API
- [clipboard](#clipboard)
- [react](#react)
- [dom](#dom)
- [date](#date)
- [types](#types)
- [algorithm](#algorithm)
- [file](#file)
- [support](#support)
- [timer](#timer)
- [operator](#operator)
- [decimal](#decimal)
- [object](#object)
- [array](#array)
- [number](#number)
- [echarts](#echarts)### clipboard
- `writeImage(element: HTMLImageElement | null | string): Promise`
复制图片到剪贴板
- `writeText(text: string): Promise`
复制文本到剪切板
### react
- `render
(element: ReactElement
): Promise`
渲染`React`组件,返回HTML字符串。
- `cleanup(): void`
清理函数,需要在调用`render()`函数后调用。
- `useDisableContextMenu(target: ContextMenuTarget = defaultContextMenuTarget): void`
在`target`函数返回的元素上禁用右键菜单,默认的`target`是`() => document`
例1:在`id`是`test`的元素上禁用右键菜单
```tsx
import { react } from '@d-matrix/utils';const TestComp = () => {
react.useDisableContextMenu(() => document.getElementById('test'));return (
此元素的右键菜单被禁用
)
}
```例2:在`document`上禁用右键菜单
```tsx
const TestComp = () => {
react.useDisableContextMenu();return
内容
}
```- `useStateCallback(initialState: T): [T, (state: T, cb?: (state: T) => void) => void]`
返回值`setState()`函数类似类组件中的`setState(updater[, callback])`,可以在`callback`中获取更新后的`state`
- `useIsMounted(): () => boolean`
获取当前组件是否已挂载的 Hook
```ts
const Test = () => {
const isMounted = useIsMounted();useEffect(() => {
if (isMounted()) {
console.log('component mounted')
}
}, [isMounted]);return null
};
```- `useCopyToClipboard(props?: UseCopyToClipboardProps)`
复制文本到剪切板, 用法见[测试](./tests/react.cy.tsx)
- `EnhancedComponent.prototype.setStateAsync(state)`
`setState()`方法的同步版本
```ts
import { react } from '@d-matrix/utils';class TestComponent extends EnhancedComponent {
state = {
pageIndex: 1,
};async onClick() {
await this.setStateAsync({ pageIndex: 2 });
console.log(this.state.pageIndex); // 2
}render() {
return (
this.onClick()}>
click
);
}
}
```- `useDeepCompareRef(deps: DependencyList): React.MutableRefObject`
深比较`deps`。返回`ref`,`ref.current`是一个自增数字,每次`deps`变化,`ref.current`加`1`。用法见[测试](./tests/react.cy.tsx)
- `InferRef`
推导子组件的`ref`类型,适用于组件没有导出其`ref`类型的场景, 更多用法见[测试](./tests/react-types.tsx)
```tsx
interface ChildRefProps {
prop1: () => void;
prop2: () => void;
}interface ChildProps {
otherProp: string;
}const Child = React.forwardRef((props, ref) => {
React.useImperativeHandle(
ref,
() => ({
prop1() {},
prop2() {},
}),
[],
);return null;
});type InferredChildRef = InferRef; // 等价于ChildRefProps
const Parent = () => {
const childRef = React.useRef(null);return ;
};
```- `useForwardRef = (ref: ForwardedRef, initialValue: any = null): React.MutableRefObject`
解决使用`React.forwardRef`后,在调用`ref.current.someMethod()`时, 出现`Property 'current' does not exist on type '(instance: HTMLInputElement | null) => void'` TS类型错误,具体问题见[这里](https://stackoverflow.com/questions/66060217/i-cant-type-the-ref-correctly-using-useref-hook-in-typescript)
```ts
const Input = React.forwardRef>((props, ref) => {
const forwardRef = useForwardRef(ref);
useEffect(() => {
forwardRef.current.focus();
});
return ;
});
```- `useMediaQuery(query, options?): boolean`
使用[Match Media API](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) 检测当前document是否匹配media query
```ts
import { useMediaQuery } from '@d-matrix/utils/react'export default function Component() {
const matches = useMediaQuery('(min-width: 768px)')return (
{`The view port is ${matches ? 'at least' : 'less than'} 768 pixels wide`}
)
}
```### dom
- `scrollToTop(element: Element | null | undefined): void`
元素滚动条滚动到顶部,对老旧浏览器做了兼容,见[浏览器兼容性](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop#browser_compatibility)。
- `strip(html: string): string`
从字符串中去除 HTML 标签并返回纯文本内容。
```ts
import { dom } from '@d-matrix/utils';dom.strip('测试高亮测试'); // '测试高亮测试'
```### date
- `rangeOfYears(start: number, end: number = new Date().getFullYear()): number[]`
创建`start`和`end`之间的年份数组。
- `getYears()`
```ts
export interface YearOption {
label: string;
value: number;
}export enum YearOptionKind {
Numbers,
Objects,
}export type GetYearsOptions = {
// 开始年份
startYear?: number;
// 最近几年
recentYears?: number;
// 截止年份
endYear?: number;
// 后缀,默认为'年'
suffix?: string;
};export function getYears(options: GetYearsOptions & { type: YearOptionKind.Numbers }): number[];
export function getYears(options: GetYearsOptions & { type: YearOptionKind.Objects }): YearOption[];
export function getYears(options: GetYearsOptions & { type: YearOptionKind }): number[] | YearOption[]
```获取n年,`type`传`YearOptionKind.Numbers`,返回`[2023, 2022, 2021]`数字数组;`type`传`YearOptionKind.Objects`,返回如下的对象数组
```sh
[
{ value: 2023, label: '2023年' },
{ value: 2022, label: '2022年' },
{ value: 2021, label: '2021年' },
]
```更多用法,见[测试用例](./tests/date.cy.ts)
- `dayOfWeek(num: number, lang: keyof typeof i18n = 'zh'): string`
返回星期几, `lang`仅支持`zh`和`en`, `num`必须为正整数,否则报错
```js
dayOfWeek(0) // "日"
```### types
- `WithOptional`
```ts
type A = { a: number; b: number; c: number; };
type T0 = WithOptional; // { a: number; b?: number; c?: number }
```- `FunctionPropertyNames`
获取对象中的方法名称,返回union type
```ts
class A {
add() {}
minus() {}
div() {}
public result: number = 0;
}
type T0 = FunctionPropertyNames; // 'add' | 'minus' | 'div'const t1 = {
add() {},
minus() {},
div() {},
result: 0,
};
type T1 = FunctionPropertyNames; // 'add' | 'minus' | 'div'
```- `NonFunctionPropertyNames`
获取对象中非函数属性名称,返回union type
```ts
class A {
add() {}
minus() {}
div() {}
public result: number = 0;
}
type T0 = FunctionPropertyNames; // 'result'const t1 = {
add() {},
minus() {},
div() {},
result: 0,
};
type T1 = FunctionPropertyNames; // 'result'
```- `ValueOf`
获取对象中`key`的值,返回由这些值组成的union type
```ts
const map = {
0: '0m',
1: '1m',
2: '2m',
3: '3m',
4: '4m',
5: '5m',
6: '6m',
} as const;type T0 = ValueOf; // '0m' | '1m' | '2m' | '3m' | '4m' | '5m' | '6m'
```- `WithRequired`
指定属性变为必选
```ts
type Input = {
a: number;
b?: string;
};
type Output = WithRequired // { a: number; b: string }
```### algorithm
- `function nodeCountAtDepth(root: Record, depth: number, childrenKey: string = 'children'): number;`
计算指定层级的节点数量
```ts
const root = {
id: 1,
children: [
{ id: 2, children: [{ id: 21 }, { id: 22 }, { id: 23 }] },
{ id: 3, children: [{ id: 31 }, { id: 32 }, { id: 33 }] },
],
};
expect(tree.nodeCountAtDepth(root, 0)).to.be.equal(1);
expect(tree.nodeCountAtDepth(root, 1)).to.be.equal(2);
expect(tree.nodeCountAtDepth(root, 2)).to.be.equal(6);
```- `const findNode = >(tree: T[], predicate: (node: T) => boolean, childrenKey = 'children'): T | null`
找到符合条件的节点
```ts
const root = {
id: 1,
children: [
{ id: 2, children: [{ id: 21 }, { id: 22 }, { id: 23 }] },
{ id: 3, children: [{ id: 31 }, { id: 32 }, { id: 33 }] },
],
};
const actual = tree.findNode([root], (node) => node.id === 3);
expect(actual).to.be.deep.equal(root.children[1]);const actual2 = tree.findNode([root], (node) => node.id === 33);
expect(actual2).to.be.deep.equal(root.children[1].children[2]);
```### file
- `toImage(file: BlobPart | FileURL, options?: BlobPropertyBag): Promise`
转换BlobPart或者文件地址为图片对象
- `validateImageSize(file: BlobPart | FileURL, limitSize: { width: number; height: number }, options?: BlobPropertyBag): Promise`
返回值:
```ts
interface ImageSizeValidationResult {
isOk: boolean;
width: number;
height: number;
}
```图片宽,高校验
- `isImageExists(src: string, img: HTMLImageElement = new Image()): Promise`
检测图片地址是否可用
```ts
import { file } from '@d-matrix/utils';const url = 'https://picsum.photos/200/300';
const res = await file.isImageExists(url);
```传入HTML中已经存在的`img`元素
```js
import { file } from '@d-matrix/utils';const $img = document.getElementById('img');
const res = await file.isImageExists(url, $img);
```- `getFilenameFromContentDispositionHeader(header: { ['content-disposition']: string }): string`
从`Content-Disposition` response header中获取`filename`
```ts
import { file } from '@d-matrix/utils';const header = {
'content-disposition': 'attachment;filename=%E5%A4%A7%E8%A1%8C%E6%8C%87%E5%AF%BC2024-06-27-2024-06-28.xlsx'
};
const filename = file.getFilenameFromContentDispositionHeader(header);
// '大行指导2024-06-27-2024-06-28.xlsx'
```- `download(source: string | Blob, fileName = '', target?: HyperLinkTarget): void`
文件下载,`source`是文件地址或`blob`对象。
```ts
type HyperLinkTarget = "_self" | "_blank" | "_parent" | "_top"
```- `downloadFileByIframe(source: string): boolean`
通过创建`iframe`进行文件下载
## support
- `isBrowserEnv(): boolean`
是否是浏览器环境
- `isWebSocket(): boolean`
是否支持WebSocket
- `isSharedWorker(): boolean`
是否支持SharedWorker
## timer
- `sleep(ms?: number): Promise`
使用`setTimeout`与`Promise`实现,暂停执行`ms`毫秒
```ts
await sleep(3000); // 暂停3秒
console.log('continue'); // 继续执行
```## operator
- `trueTypeOf = (obj: unknown): string`
检查数据类型
```ts
trueTypeOf([]); // array
trueTypeOf({}); // object
trueTypeOf(''); // string
trueTypeOf(new Date()); // date
trueTypeOf(1); // number
trueTypeOf(function () {}); // function
trueTypeOf(/test/i); // regexp
trueTypeOf(true); // boolean
trueTypeOf(null); // null
trueTypeOf(undefined); // undefined
```## decimal
- `format(value: number | string | undefined | null, options?: FormatOptions): string`
格式化数字,默认保留3位小数,可添加前缀,后缀,默认值为'--',用法见[测试](./tests//decimal.cy.ts)
```ts
type FormatOptions = {
decimalPlaces?: number | false;
suffix?: string;
prefix?: string;
defaultValue?: string;
operation?: {
operator: 'add' | 'sub' | 'mul' | 'div' | 'toDecimalPlaces';
value: number;
}[];
};
```## object
- `removeZeroValueKeys = >(obj: T, zeroValues = ZeroValues): T`
移除零值的键, 默认的零值是:`undefined`、`null`, `''`, `NaN`, `[]`, `{}`
```ts
removeZeroValueKeys({ a: '', b: 'abc', c: undefined, d: null, e: NaN, f: -1, g: [], h: {} })
// { b: 'abc', f: -1 }
```- `typedKeys(obj: T): Array`
返回`tuple`,而不是`string[]`
```ts
const obj = { a: 1, b: '2' };
Object.keys(obj) // string[]
object.typedKeys({ a: 1, b: '2' }) // ('a' | 'b')[]
```## array
- `moveImmutable(array: T[], fromIndex: number, toIndex: number): T[]`
```js
import { array } from '@d-matrix/utils';const input = ['a', 'b', 'c'];
const array1 = array.moveImmutable(input, 1, 2);
console.log(array1);
//=> ['a', 'c', 'b']const array2 = array.moveImmutable(input, -1, 0);
console.log(array2);
//=> ['c', 'a', 'b']const array3 = array.moveImmutable(input, -2, -3);
console.log(array3);
//=> ['b', 'a', 'c']
```- `moveMutable(array: T[], fromIndex: number, toIndex: number): void`
- `moveToStart(array: T[], predicate: (item: T) => boolean): T[]`
移动元素到数组首位,不会修改原数组
```js
import { array } from '@d-matrix/utils';const list = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
const newList = array.moveToStart(list, (item) => item.id === 4);// [{ id: 4 }, { id: 1 }, { id: 2 }, { id: 3 }, { id: 5 }]
```- `moveMulti(arr: T[], indexes: number[], start: number): T[]`
移动多个元素到数组中指定的位置,用法,见[测试用例](tests/algorithm.cy.ts)
## number
- `randomInt(min: number, max: number): number`
返回`min`, `max`之间的随机整数
## echarts
- `mergeOption(defaults: EChartsOption, overrides: EChartsOption, option?: deepmerge.Options): EChartsOption`
deep merge Echarts配置,用法见[测试用例](./tests//echarts/echarts.cy.ts)
- `fill, XAxisField extends keyof T, YAxisField extends keyof T>(dataSource: T[], xAxisField:XAxisField, yAxisField: YAxisField): T[]`
场景:后端接口返回某几个时间点的数据,需求是在接口数据的基础上每隔5分钟补一个点,以达到图中的效果: [折线图](https://raw.githubusercontent.com/mrdulin/pic-bucket-01/master/Dingtalk_20240724140535.jpg)。
填充的点的Y轴值为前一个点的值, 时间示例: [9:23, 9:27] => [9:23, 9:25, 9:27, 9:30],更多,见[测试用例](./tests/echarts/fill.cy.ts)
- `calcYAxisRange, Key extends keyof T>(data: T[], key: Key, decimalPlaces = 2, splitNumber = 5): { max:number; min:number }`
计算echarts YAxis的max和min属性,以达到根据实际数据动态调整,使折线图的波动明显。且第一个点始终在Y轴中间位置,[效果图](https://raw.githubusercontent.com/mrdulin/pic-bucket-01/master/Dingtalk_20240724140535.jpg)
## 测试
运行全部组件测试
```bash
npm run cy:component:all
```运行单个组件测试
```bash
npm run cy:component -- tests/date.cy.ts
```运行E2E测试
将`src`通过`tsc` build到`public/dist`目录
```bash
npm run build:public
```启动一个Web服务器来访问`public/index.html`文件,`dist`目录的脚本可以通过``引入
```bash
npm run serve
```最后启动cypress GUI客户端,选择E2E测试
```bash
npm run cy:open
```## 发布
更新package version:
```bash
npm version or ...
```构建:
```bash
npm build
```发布:
```bash
npm publish --access public
```网络原因导致连接registry服务器超时,可指定proxy
```bash
npm --proxy http://127.0.0.1:7890 publish
```镜像站查询版本与手动同步:
[npm镜像站](https://npmmirror.com/package/@d-matrix/utils)
通过`git log`命令获取changelogs,用于填写GitHub Release内容:
```bash
git log --oneline --decorate
```## 注意事项
- [Before Publishing: Make Sure Your Package Installs and Works](https://docs.npmjs.com/cli/v10/using-npm/developers/#before-publishing-make-sure-your-package-installs-and-works)
- [npm-link](https://docs.npmjs.com/cli/v9/commands/npm-link)
- [Creating and publishing scoped public packages](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages)