@veltra/utils 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/dom/class-name.d.ts +10 -0
  2. package/dist/dom/class-name.js +16 -0
  3. package/dist/dom/class-name.js.map +1 -0
  4. package/dist/dom/highlight.d.ts +14 -0
  5. package/dist/dom/highlight.js +19 -0
  6. package/dist/dom/highlight.js.map +1 -0
  7. package/dist/dom/position.d.ts +28 -0
  8. package/dist/dom/position.js +59 -0
  9. package/dist/dom/position.js.map +1 -0
  10. package/dist/dom/style.d.ts +25 -0
  11. package/dist/dom/style.js +38 -0
  12. package/dist/dom/style.js.map +1 -0
  13. package/dist/dom/z-index.d.ts +9 -0
  14. package/dist/dom/z-index.js +11 -0
  15. package/dist/dom/z-index.js.map +1 -0
  16. package/dist/form/validate.d.ts +33 -0
  17. package/dist/form/validate.js +197 -0
  18. package/dist/form/validate.js.map +1 -0
  19. package/dist/helper/create-increase.d.ts +10 -0
  20. package/dist/helper/create-increase.js +17 -0
  21. package/dist/helper/create-increase.js.map +1 -0
  22. package/dist/helper/create-toggle.d.ts +15 -0
  23. package/dist/helper/create-toggle.js +24 -0
  24. package/dist/helper/create-toggle.js.map +1 -0
  25. package/dist/helper/frame.d.ts +9 -0
  26. package/dist/helper/frame.js +14 -0
  27. package/dist/helper/frame.js.map +1 -0
  28. package/dist/helper/make-bem.d.ts +52 -0
  29. package/dist/helper/make-bem.js +39 -0
  30. package/dist/helper/make-bem.js.map +1 -0
  31. package/dist/helper/tween.d.ts +43 -0
  32. package/dist/helper/tween.js +89 -0
  33. package/dist/helper/tween.js.map +1 -0
  34. package/dist/helper/vue.d.ts +44 -0
  35. package/dist/helper/vue.js +62 -0
  36. package/dist/helper/vue.js.map +1 -0
  37. package/dist/index.d.ts +19 -0
  38. package/dist/index.js +15 -0
  39. package/dist/reactive/proxy.d.ts +19 -0
  40. package/dist/reactive/proxy.js +57 -0
  41. package/dist/reactive/proxy.js.map +1 -0
  42. package/dist/shared/constants.d.ts +10 -0
  43. package/dist/shared/constants.js +11 -0
  44. package/dist/shared/constants.js.map +1 -0
  45. package/dist/types/component-common.d.ts +37 -0
  46. package/dist/types/form-context.d.ts +6 -0
  47. package/dist/types/helper.d.ts +22 -0
  48. package/dist/types/index.d.ts +5 -0
  49. package/dist/types/index.js +0 -0
  50. package/dist/types/utils/form/validate.d.ts +34 -0
  51. package/package.json +47 -0
  52. package/src/dom/class-name.ts +20 -0
  53. package/src/dom/highlight.ts +23 -0
  54. package/src/dom/position.ts +96 -0
  55. package/src/dom/style.ts +60 -0
  56. package/src/dom/z-index.ts +7 -0
  57. package/src/env.d.ts +1 -0
  58. package/src/form/validate.ts +278 -0
  59. package/src/helper/create-increase.ts +14 -0
  60. package/src/helper/create-toggle.ts +29 -0
  61. package/src/helper/frame.ts +9 -0
  62. package/src/helper/make-bem.ts +109 -0
  63. package/src/helper/tween.ts +123 -0
  64. package/src/helper/vue.ts +88 -0
  65. package/src/index.ts +28 -0
  66. package/src/reactive/proxy.ts +90 -0
  67. package/src/shared/constants.ts +8 -0
  68. package/src/shared/index.ts +1 -0
  69. package/src/types/component-common.ts +41 -0
  70. package/src/types/form-context.ts +2 -0
  71. package/src/types/helper.ts +31 -0
  72. package/src/types/index.ts +4 -0
  73. package/src/types/utils/form/validate.ts +33 -0
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@veltra/utils",
3
+ "version": "1.0.0",
4
+ "files": [
5
+ "dist",
6
+ "src"
7
+ ],
8
+ "type": "module",
9
+ "sideEffects": [
10
+ "*.css"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "development": "./src/index.ts",
16
+ "import": "./dist/index.js"
17
+ },
18
+ "./types": {
19
+ "types": "./dist/types/index.d.ts",
20
+ "development": "./src/types/index.ts",
21
+ "import": "./dist/types/index.js"
22
+ },
23
+ "./types/*": {
24
+ "types": "./dist/types/*.d.ts",
25
+ "development": "./src/types/*",
26
+ "import": "./dist/types/*.js"
27
+ },
28
+ "./*": {
29
+ "types": "./dist/*.d.ts",
30
+ "development": "./src/*",
31
+ "import": "./dist/*.js"
32
+ }
33
+ },
34
+ "scripts": {
35
+ "build": "tsdown",
36
+ "check-types": "tsc -p tsconfig.json --noEmit"
37
+ },
38
+ "dependencies": {
39
+ "@cat-kit/core": "^1.0.1"
40
+ },
41
+ "devDependencies": {
42
+ "tsdown": "^0.21.7"
43
+ },
44
+ "peerDependencies": {
45
+ "vue": "^3.5.0"
46
+ }
47
+ }
@@ -0,0 +1,20 @@
1
+ import { makeBEM, type BEMFactory } from '../helper/make-bem'
2
+ import { CLS_PREFIX } from '../shared/constants'
3
+
4
+ export const bem: BEMFactory<typeof CLS_PREFIX> = makeBEM(CLS_PREFIX)
5
+
6
+ export function addClass(el: HTMLElement, className: string | string[]): void {
7
+ if (Array.isArray(className)) {
8
+ className.forEach((c) => el.classList.add(c))
9
+ } else {
10
+ el.classList.add(className)
11
+ }
12
+ }
13
+
14
+ export function removeClass(el: HTMLElement, className: string | string[]): void {
15
+ if (Array.isArray(className)) {
16
+ className.forEach((c) => el.classList.remove(c))
17
+ } else {
18
+ el.classList.remove(className)
19
+ }
20
+ }
@@ -0,0 +1,23 @@
1
+ // TODO: 在多种场景下, 比较KMP算法, BM算法, two-way算法是否比正则更快
2
+
3
+ interface HighlightChunk {
4
+ text: string
5
+ highlight: boolean
6
+ }
7
+
8
+ const escapeRegexp = (term: string): string =>
9
+ term.replace(/[|\\{}()[\]^$+*?.-]/g, (char: string) => `\\${char}`)
10
+
11
+ /**
12
+ * 获取文本高亮片段
13
+ * @param string 字符串
14
+ * @param substrings 需要匹配的字串列表
15
+ */
16
+ export function getHighlightChunks(str: string, substrings: string[]): HighlightChunk[] {
17
+ const _substrings = substrings.filter((s) => !!s).map((s) => escapeRegexp(s.trim()))
18
+ const re = new RegExp(`(${_substrings.join('|')})`, 'gi')
19
+ return str
20
+ .split(re)
21
+ .filter(Boolean)
22
+ .map((text) => ({ text, highlight: re.test(text) }))
23
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * 获取可滚动的父级
3
+ * @param el 元素
4
+ * @returns
5
+ */
6
+ export function getScrollParents(el: HTMLElement): HTMLElement[] {
7
+ const parents: HTMLElement[] = []
8
+ let parent = el.parentElement
9
+ while (parent) {
10
+ if (parent.scrollHeight > parent.clientHeight) {
11
+ parents.push(parent)
12
+ }
13
+
14
+ parent = parent.parentElement
15
+ }
16
+ return parents
17
+ }
18
+
19
+ /**
20
+ * 获取最近的可滚动父级
21
+ * @param el 元素
22
+ * @returns 最近的可滚动父级
23
+ */
24
+ export function getNearestScrollParent(el: HTMLElement): HTMLElement | null {
25
+ let parent = el.parentElement
26
+ while (parent) {
27
+ if (parent.scrollHeight > parent.clientHeight) {
28
+ return parent
29
+ }
30
+ parent = parent.parentElement
31
+ }
32
+ return null
33
+ }
34
+
35
+ type ScrollViewPosition = 'center' | 'start' | 'end'
36
+
37
+ /**
38
+ * 滚动元素到容器视图中
39
+ * @description 用于替代 `el.scrollIntoView` 方法,因为 `el.scrollIntoView` 在某些情况下会导致外部元素滚动
40
+ * @param el 元素
41
+ * @param container 可滚动容器
42
+ * @param options 滚动选项
43
+ */
44
+ export function scrollIntoContainerView(
45
+ el: HTMLElement,
46
+ container: HTMLElement | null,
47
+ options?: { block?: ScrollViewPosition; inline?: ScrollViewPosition }
48
+ ): void {
49
+ container = container || getNearestScrollParent(el)
50
+ if (!container) return
51
+
52
+ const { block = 'center', inline = 'center' } = options || {}
53
+
54
+ const {
55
+ offsetTop: eOffsetTop,
56
+ offsetLeft: eOffsetLeft,
57
+ offsetHeight: eOffsetHeight,
58
+ offsetWidth: eOffsetWidth
59
+ } = el
60
+
61
+ const {
62
+ clientHeight: cClientHeight,
63
+ clientWidth: cClientWidth,
64
+ scrollTop: cScrollTop,
65
+ scrollLeft: cScrollLeft
66
+ } = container
67
+
68
+ const isVerticalInView =
69
+ cScrollTop + cClientHeight > eOffsetTop + eOffsetHeight && cScrollTop < eOffsetTop
70
+
71
+ const isHorizontalInView =
72
+ cScrollLeft + cClientWidth > eOffsetLeft + eOffsetWidth && cScrollLeft < eOffsetLeft
73
+
74
+ // 垂直方向和水平方向都已经在视图中,则不进行滚动
75
+ if (isVerticalInView && isHorizontalInView) return
76
+
77
+ if (!isVerticalInView) {
78
+ if (block === 'center') {
79
+ container.scrollTop = eOffsetTop - cClientHeight / 2 + eOffsetHeight / 2
80
+ } else if (block === 'start') {
81
+ container.scrollTop = eOffsetTop
82
+ } else if (block === 'end') {
83
+ container.scrollTop = eOffsetTop - cClientHeight + eOffsetHeight
84
+ }
85
+ }
86
+
87
+ if (!isHorizontalInView) {
88
+ if (inline === 'center') {
89
+ container.scrollLeft = eOffsetLeft - cClientWidth / 2 + eOffsetWidth / 2
90
+ } else if (inline === 'start') {
91
+ container.scrollLeft = eOffsetLeft
92
+ } else if (inline === 'end') {
93
+ container.scrollLeft = eOffsetLeft - cClientWidth + eOffsetWidth
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,60 @@
1
+ import { str } from '@cat-kit/core'
2
+ import type { CSSProperties } from 'vue'
3
+
4
+ /**
5
+ * 给数值加上单位
6
+ * @param value 数值
7
+ * @param unit 单位
8
+ * @returns
9
+ */
10
+ export function withUnit(value: number | string | undefined, unit: string): string | undefined {
11
+ return value === undefined
12
+ ? undefined
13
+ : typeof value === 'number' || !isNaN(+value)
14
+ ? String(value) + unit
15
+ : value
16
+ }
17
+
18
+ /**
19
+ * 设置元素样式,优先使用高性能的方式
20
+ * @param el 元素
21
+ * @param styles 样式
22
+ */
23
+ export function setStyles(el: HTMLElement, styles: CSSProperties): void {
24
+ Object.keys(styles).forEach((key) => {
25
+ el.style[key] = styles[key]
26
+ })
27
+ // TODO: 此处有问题,在某些情况下会导致样式设置失效
28
+ // 例如在 Tabs 组件中无法设置overflow属性
29
+ // if (el.attributeStyleMap) {
30
+ // Object.keys(styles).forEach(key => {
31
+ // const value = styles[key]
32
+ // if (!value && value !== 0) {
33
+ // el.attributeStyleMap.delete(kebabCase(key))
34
+ // } else {
35
+ // el.attributeStyleMap.set(kebabCase(key), value)
36
+ // }
37
+ // })
38
+ // } else {
39
+ // Object.keys(styles).forEach(key => {
40
+ // el.style[key] = styles[key]
41
+ // })
42
+ // }
43
+ }
44
+
45
+ /**
46
+ * 移除样式
47
+ * @param el dom元素
48
+ * @param props 要移除的样式属性
49
+ */
50
+ export function removeStyles(el: HTMLElement, props: string[]): void {
51
+ if (el.attributeStyleMap) {
52
+ props.forEach((key) => {
53
+ el.attributeStyleMap.delete(str(key).kebabCase())
54
+ })
55
+ } else {
56
+ props.forEach((key) => {
57
+ el.style.removeProperty(key)
58
+ })
59
+ }
60
+ }
@@ -0,0 +1,7 @@
1
+ import { createIncrease } from '../helper/create-increase'
2
+
3
+ /**
4
+ * z轴层级
5
+ * 保证每个新打开的弹框的位置都处于上层
6
+ */
7
+ export const zIndex: () => number = createIncrease(1000)
package/src/env.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module '*.scss' {}
@@ -0,0 +1,278 @@
1
+ import { o } from '@cat-kit/core'
2
+
3
+ import type { Undef } from '../types/helper'
4
+ import type { ValidateRule, Data, ValidatorConfig, PresetRule } from '../types/utils/form/validate'
5
+
6
+ const isEmpty = (value: any): value is null | undefined => {
7
+ return value === null || value === undefined
8
+ }
9
+
10
+ const presetRules: Record<PresetRule, (value: string) => string | undefined> = {
11
+ email(v) {
12
+ const re = /^([\w_-]+)@([\w-]+[.]?)*[\w]+\.[a-zA-Z]{2,10}$/
13
+ if (!re.test(v)) {
14
+ return '邮箱格式不正确'
15
+ }
16
+ },
17
+ phone(v) {
18
+ const re = /^\d{11}$/
19
+ if (!re.test(v)) {
20
+ return '手机号格式不正确'
21
+ }
22
+ },
23
+ num(v) {
24
+ const re = /^\d+$/
25
+ if (!re.test(v)) {
26
+ return '数字格式不正确'
27
+ }
28
+ },
29
+ url(v) {
30
+ const re = /^(ftp|https?):\/\/([\w_-]+)\.([\w-]+[.]?)*[\w]+\.[a-zA-Z]{2,10}(.*)/
31
+ if (!re.test(v)) {
32
+ return '链接格式不正确'
33
+ }
34
+ },
35
+
36
+ idCard(v) {
37
+ const re = /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$/
38
+ if (!re.test(v)) {
39
+ return '身份证格式不正确'
40
+ }
41
+ }
42
+ }
43
+
44
+ /** 预设规则 */
45
+ const ruleTypes = {
46
+ required(value: any, required: ValidateRule['required']): Undef<string> {
47
+ if (required === false) return
48
+
49
+ const errMsg = typeof required === 'string' ? required : '该项不能为空'
50
+ if (isEmpty(value)) return errMsg
51
+
52
+ if (Array.isArray(value) && !value.length) return errMsg
53
+ if (typeof value === 'string' && !value) return errMsg
54
+ },
55
+ min(value: any, rule: ValidateRule['min']): Undef<string> {
56
+ if (isEmpty(value)) return
57
+ let _rule = Array.isArray(rule) ? rule[0] : rule!
58
+ let errMsg = Array.isArray(rule) ? rule[1] : `该项必须大于等于${_rule}`
59
+ if (typeof value !== 'number') return `${value}不是一个数字`
60
+ if (value < _rule) return errMsg
61
+ },
62
+ max(value: any, rule: ValidateRule['max']): Undef<string> {
63
+ if (isEmpty(value)) return
64
+ let _rule = Array.isArray(rule) ? rule[0] : rule!
65
+ let errMsg = Array.isArray(rule) ? rule[1] : `该项必须小于等于${_rule}`
66
+ if (typeof value !== 'number') return `${value}不是一个数字`
67
+ if (value > _rule) return errMsg
68
+ },
69
+
70
+ minLen(value: any, rule: ValidateRule['minLen']): Undef<string> {
71
+ if (isEmpty(value)) return
72
+ let _rule = Array.isArray(rule) ? rule[0] : rule!
73
+ let errMsg = Array.isArray(rule) ? rule[1] : `该项长度必须大于等于${_rule}`
74
+ if (!Array.isArray(value) && typeof value !== 'string') return `${value}不是一个字符串或数组`
75
+ if (value.length < _rule) return errMsg
76
+ },
77
+ maxLen(value: any, rule: ValidateRule['maxLen']): Undef<string> {
78
+ if (isEmpty(value)) return
79
+ let _rule = Array.isArray(rule) ? rule[0] : rule!
80
+ let errMsg = Array.isArray(rule) ? rule[1] : `该项长度必须小于等于:${_rule}`
81
+ if (!Array.isArray(value) && typeof value !== 'string') return `${value}不是一个字符串或数组`
82
+ if (value.length > _rule) return errMsg
83
+ },
84
+ match(value: any, rule: ValidateRule['match']): Undef<string> {
85
+ if (isEmpty(value) || value === '') return
86
+ if (typeof rule === 'string') {
87
+ if (!rule) return
88
+ rule = new RegExp(rule)
89
+ }
90
+ let _rule = Array.isArray(rule) ? rule[0] : rule!
91
+ let errMsg = Array.isArray(rule) ? rule[1] : `该项不匹配正则:${_rule}`
92
+ if (typeof value !== 'string') return `${value}不是一个字符串`
93
+ if (!_rule.test(value)) return errMsg
94
+ },
95
+ preset(value: any, rule: ValidateRule['preset']): Undef<string> {
96
+ if (isEmpty(value) || value === '' || !rule) return
97
+ if (typeof value !== 'string') return `${value}不是一个字符串`
98
+ const ruleValidator = presetRules[rule]
99
+ return ruleValidator(value)
100
+ }
101
+ }
102
+ /** */
103
+ export class Validator<
104
+ Rules extends Record<string, ValidateRule> = Record<string, ValidateRule>,
105
+ Field extends keyof Rules = keyof Rules
106
+ > {
107
+ protected rules: Rules
108
+
109
+ private config?: ValidatorConfig
110
+
111
+ constructor(rules: Rules, config?: ValidatorConfig) {
112
+ this.rules = rules
113
+ if (config) {
114
+ this.config = config
115
+ }
116
+ }
117
+
118
+ /** 是否存在规则 */
119
+ private get existRules() {
120
+ for (const _ in this.rules) {
121
+ return true
122
+ }
123
+ return false
124
+ }
125
+
126
+ /**
127
+ * 校验单条数据
128
+ * @param data 数据
129
+ */
130
+ private async validateSingleData(
131
+ data: Record<any, any>,
132
+ fields?: Field | Field[]
133
+ ): Promise<{ [key in Field]?: string[] }> {
134
+ const fieldErrors: { [key in Field]?: string[] } = {}
135
+
136
+ if (!this.existRules) return fieldErrors
137
+
138
+ const lazy = this.config?.lazy ?? true
139
+
140
+ fields = fields
141
+ ? Array.isArray(fields)
142
+ ? fields
143
+ : [fields]
144
+ : (Object.keys(this.rules) as Field[])
145
+
146
+ // 懒校验,当有一个规则不通过,剩下的规则不再校验
147
+ if (lazy) {
148
+ // 校验字段
149
+ for (let i = 0; i < fields.length; i++) {
150
+ const field = fields[i]!
151
+ // oxlint-disable-next-line eslint(no-await-in-loop)
152
+ const errors = await this.validateValueLazy(data, field)
153
+ if (errors.length === 0) continue
154
+ fieldErrors[field] = errors
155
+ }
156
+ return fieldErrors
157
+ } else {
158
+ // 校验字段
159
+ for (let i = 0; i < fields.length; i++) {
160
+ const field = fields[i]!
161
+ // oxlint-disable-next-line eslint(no-await-in-loop)
162
+ const errors = await this.validateValue(data, field)
163
+ if (errors.length === 0) continue
164
+ fieldErrors[field] = errors
165
+ }
166
+
167
+ return fieldErrors
168
+ }
169
+ }
170
+
171
+ private async validateValue(data: Record<any, any>, field: Field) {
172
+ const rules = this.rules[field]!
173
+ const value = o(data).get(field as string)
174
+
175
+ const { validator, required, ...normalRules } = rules
176
+
177
+ let errors: string[] = []
178
+
179
+ // 必填要先去校验
180
+ if (required) {
181
+ const err = ruleTypes.required(value, required)
182
+ err && errors.push(err)
183
+ }
184
+
185
+ // 校验规则
186
+ for (const ruleKey in normalRules) {
187
+ const validate = ruleTypes[ruleKey]
188
+ if (!validate) continue
189
+ const err = validate(value, normalRules[ruleKey])
190
+ err && errors.push(err)
191
+ }
192
+
193
+ // 自定义校验
194
+ if (validator) {
195
+ const err = await validator(value, data)
196
+ err && errors.push(err)
197
+ }
198
+
199
+ return errors
200
+ }
201
+
202
+ private async validateValueLazy(data: Record<any, any>, field: Field): Promise<string[]> {
203
+ const rules = this.rules[field]
204
+ const value = o(data).get(field as string)
205
+ let errors: string[] = []
206
+
207
+ if (!rules) return errors
208
+
209
+ const { validator, required, ...normalRules } = rules
210
+
211
+ // 必填要先去校验
212
+ if (required) {
213
+ const err = ruleTypes.required(value, required)
214
+ if (err) {
215
+ errors.push(err)
216
+ return errors
217
+ }
218
+ }
219
+
220
+ // 校验规则
221
+ for (const ruleKey in normalRules) {
222
+ const validate = ruleTypes[ruleKey]
223
+ if (!validate) continue
224
+
225
+ const err = validate(value, normalRules[ruleKey])
226
+
227
+ if (err) {
228
+ errors.push(err)
229
+ return errors
230
+ }
231
+ }
232
+
233
+ // 自定义校验最火校验
234
+ if (validator) {
235
+ const err = await validator(value, data)
236
+ if (err) {
237
+ errors.push(err)
238
+ return errors
239
+ }
240
+ }
241
+
242
+ return errors
243
+ }
244
+
245
+ /**
246
+ * 校验多条数据
247
+ * @param field 需要校验的字段
248
+ */
249
+ private async validateManyData(
250
+ data: Data,
251
+ field?: Field | Field[]
252
+ ): Promise<{ [key in Field]?: string[] }> {
253
+ let i = 0
254
+ while (i < data.length) {
255
+ const item = data[i]!
256
+ // oxlint-disable-next-line eslint(no-await-in-loop)
257
+ const fieldErrors = await this.validateSingleData(item, field)
258
+ if (Object.keys(fieldErrors).length > 0) {
259
+ return fieldErrors
260
+ }
261
+ i++
262
+ }
263
+
264
+ return {}
265
+ }
266
+
267
+ /**
268
+ * 校验
269
+ * @param data 数据
270
+ * @param fields 字段
271
+ * @returns
272
+ */
273
+ async validate(data: Data, fields?: Field | Field[]): Promise<{ [key in Field]?: string[] }> {
274
+ return Array.isArray(data)
275
+ ? this.validateManyData(data, fields)
276
+ : this.validateSingleData(data, fields)
277
+ }
278
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 创建一个自增函数
3
+ * @param initial 初始值
4
+ * @returns
5
+ */
6
+ export function createIncrease(initial = 1000): () => number {
7
+ let value = initial
8
+
9
+ const increase = () => {
10
+ return value++
11
+ }
12
+
13
+ return increase
14
+ }
@@ -0,0 +1,29 @@
1
+ type Active = boolean | ((active: boolean) => boolean) | ((active: boolean) => Promise<boolean>)
2
+
3
+ type ToggleReturn = [{ value: boolean }, (active: Active) => void]
4
+ /**
5
+ * 创建一个toggle函数
6
+ * @param initial 初始值
7
+ * @param onChange 值变化时的回调
8
+ * @returns
9
+ */
10
+ export function createToggle(initial = false, onChange?: (active: boolean) => void): ToggleReturn {
11
+ const state = { value: initial }
12
+
13
+ function toggle(active: Active) {
14
+ if (typeof active === 'boolean') {
15
+ state.value = active
16
+ return onChange?.(state.value)
17
+ }
18
+
19
+ const result = active(state.value)
20
+
21
+ if (result instanceof Promise) {
22
+ result.then(toggle)
23
+ } else {
24
+ toggle(result)
25
+ }
26
+ }
27
+
28
+ return [state, toggle]
29
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 下一帧运行
3
+ * @param cb 回调
4
+ */
5
+ export function nextFrame(cb: () => void): void {
6
+ requestAnimationFrame(() => {
7
+ requestAnimationFrame(cb)
8
+ })
9
+ }
@@ -0,0 +1,109 @@
1
+ /** BEM实例 */
2
+ export type BEM<N extends string, P extends string = 'u-', B extends string = `${P}${N}`> = {
3
+ /** BEM中的块 */
4
+ b: B
5
+
6
+ /**
7
+ * BEM中的元素(E)
8
+ * @param name 元素名称
9
+ * @returns
10
+ */
11
+ e<const E extends string>(name: E): `${B}__${E}`
12
+
13
+ /**
14
+ * 基于当前bem创建一个新的bem
15
+ * @param block 块名称
16
+ */
17
+ create<const Block extends string>(block: Block): BEM<`${N}-${Block}`, P>
18
+
19
+ /**
20
+ * BEM中的修饰符(M)
21
+ * @param m 修饰符名
22
+ * @returns 修饰符
23
+ */
24
+ m<const M extends string>(m: M): `${B}--${M}`
25
+
26
+ /**
27
+ * BEM中的元素与修饰符(E--M)
28
+ * @param e 元素名
29
+ * @param m 修饰符名
30
+ * @returns
31
+ */
32
+ em<const E extends string, const M extends string>(e: E, m: M): `${B}__${E}--${M}`
33
+ }
34
+
35
+ /** BEM工厂 */
36
+ export interface BEMFactory<Prefix extends string> {
37
+ <N extends string>(name: N): BEM<N, Prefix>
38
+ /**
39
+ * 生成is辅助类
40
+ * @param name 辅助类名称
41
+ */
42
+ is<const N extends string>(name: N): `is-${N}`
43
+ /**
44
+ * 生成is辅助类
45
+ * @param name 辅助类名称
46
+ * @param condition 辅助类显示条件
47
+ */
48
+ is<const N extends string, C extends boolean | undefined>(
49
+ name: N,
50
+ condition: C
51
+ ): C extends true ? `is-${N}` : ''
52
+ }
53
+
54
+ /**
55
+ * 创建一个bem函数
56
+ * @param prefix 前缀
57
+ */
58
+ export function makeBEM<Prefix extends '' | `${string}-`>(prefix: Prefix): BEMFactory<Prefix> {
59
+ /**
60
+ * css类命名辅助
61
+ * @param name 类block名称
62
+ */
63
+ function bem<N extends string>(name: N): BEM<N, Prefix> {
64
+ const b = `${prefix}${name}` as BEM<N, Prefix>['b']
65
+ return {
66
+ b,
67
+
68
+ e(name) {
69
+ return `${b}__${name}`
70
+ },
71
+
72
+ create(block) {
73
+ return bem(`${name}-${block}`)
74
+ },
75
+
76
+ m(m) {
77
+ return `${b}--${m}`
78
+ },
79
+
80
+ em(e, m) {
81
+ return `${b}__${e}--${m}`
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 生成is辅助类
88
+ * @param name 辅助类名称
89
+ */
90
+ function is<const N extends string>(name: N): `is-${N}`
91
+ /**
92
+ * 生成is辅助类
93
+ * @param name 辅助类名称
94
+ * @param condition 辅助类显示条件
95
+ */
96
+ function is<const N extends string, C extends boolean | undefined>(
97
+ name: N,
98
+ condition: C
99
+ ): C extends true ? `is-${N}` : ''
100
+
101
+ function is<N extends string>(name: N, condition?: boolean) {
102
+ if (arguments.length < 2) return `is-${name}`
103
+ return condition !== true ? '' : (`is-${name}` as const)
104
+ }
105
+
106
+ bem.is = is
107
+
108
+ return bem
109
+ }