@planet-matrix/mobius-model 0.4.0 → 0.6.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.
- package/CHANGELOG.md +32 -0
- package/README.md +134 -21
- package/dist/index.js +45 -4
- package/dist/index.js.map +186 -11
- package/oxlint.config.ts +6 -0
- package/package.json +16 -10
- package/src/abort/README.md +92 -0
- package/src/abort/abort-manager.ts +278 -0
- package/src/abort/abort-signal-listener-manager.ts +81 -0
- package/src/abort/index.ts +2 -0
- package/src/basic/README.md +69 -117
- package/src/basic/enhance.ts +10 -0
- package/src/basic/function.ts +81 -62
- package/src/basic/index.ts +2 -0
- package/src/basic/is.ts +152 -71
- package/src/basic/object.ts +82 -0
- package/src/basic/promise.ts +29 -8
- package/src/basic/string.ts +2 -33
- package/src/color/README.md +105 -0
- package/src/color/index.ts +3 -0
- package/src/color/internal.ts +42 -0
- package/src/color/rgb/analyze.ts +236 -0
- package/src/color/rgb/construct.ts +130 -0
- package/src/color/rgb/convert.ts +227 -0
- package/src/color/rgb/derive.ts +303 -0
- package/src/color/rgb/index.ts +6 -0
- package/src/color/rgb/internal.ts +208 -0
- package/src/color/rgb/parse.ts +302 -0
- package/src/color/rgb/serialize.ts +144 -0
- package/src/color/types.ts +57 -0
- package/src/color/xyz/analyze.ts +80 -0
- package/src/color/xyz/construct.ts +19 -0
- package/src/color/xyz/convert.ts +71 -0
- package/src/color/xyz/index.ts +3 -0
- package/src/color/xyz/internal.ts +23 -0
- package/src/css/README.md +93 -0
- package/src/css/class.ts +559 -0
- package/src/css/index.ts +1 -0
- package/src/encoding/README.md +92 -0
- package/src/encoding/base64.ts +107 -0
- package/src/encoding/index.ts +1 -0
- package/src/environment/README.md +97 -0
- package/src/environment/basic.ts +26 -0
- package/src/environment/device.ts +311 -0
- package/src/environment/feature.ts +285 -0
- package/src/environment/geo.ts +337 -0
- package/src/environment/index.ts +7 -0
- package/src/environment/runtime.ts +400 -0
- package/src/environment/snapshot.ts +60 -0
- package/src/environment/variable.ts +239 -0
- package/src/event/README.md +90 -0
- package/src/event/class-event-proxy.ts +228 -0
- package/src/event/common.ts +19 -0
- package/src/event/event-manager.ts +203 -0
- package/src/event/index.ts +4 -0
- package/src/event/instance-event-proxy.ts +186 -0
- package/src/event/internal.ts +24 -0
- package/src/exception/README.md +96 -0
- package/src/exception/browser.ts +219 -0
- package/src/exception/index.ts +4 -0
- package/src/exception/nodejs.ts +169 -0
- package/src/exception/normalize.ts +106 -0
- package/src/exception/types.ts +99 -0
- package/src/identifier/README.md +92 -0
- package/src/identifier/id.ts +119 -0
- package/src/identifier/index.ts +2 -0
- package/src/identifier/uuid.ts +187 -0
- package/src/index.ts +18 -1
- package/src/log/README.md +79 -0
- package/src/log/index.ts +5 -0
- package/src/log/log-emitter.ts +72 -0
- package/src/log/log-record.ts +10 -0
- package/src/log/log-scheduler.ts +74 -0
- package/src/log/log-type.ts +8 -0
- package/src/log/logger.ts +543 -0
- package/src/orchestration/README.md +89 -0
- package/src/orchestration/coordination/barrier.ts +214 -0
- package/src/orchestration/coordination/count-down-latch.ts +215 -0
- package/src/orchestration/coordination/errors.ts +98 -0
- package/src/orchestration/coordination/index.ts +16 -0
- package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
- package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
- package/src/orchestration/coordination/keyed-lock.ts +168 -0
- package/src/orchestration/coordination/mutex.ts +257 -0
- package/src/orchestration/coordination/permit.ts +127 -0
- package/src/orchestration/coordination/read-write-lock.ts +444 -0
- package/src/orchestration/coordination/semaphore.ts +280 -0
- package/src/orchestration/index.ts +1 -0
- package/src/random/README.md +78 -0
- package/src/random/index.ts +1 -0
- package/src/random/string.ts +35 -0
- package/src/reactor/README.md +4 -0
- package/src/reactor/reactor-core/primitive.ts +9 -9
- package/src/reactor/reactor-core/reactive-system.ts +5 -5
- package/src/singleton/README.md +79 -0
- package/src/singleton/factory.ts +55 -0
- package/src/singleton/index.ts +2 -0
- package/src/singleton/manager.ts +204 -0
- package/src/storage/README.md +107 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/table.ts +449 -0
- package/src/timer/README.md +86 -0
- package/src/timer/expiration/expiration-manager.ts +594 -0
- package/src/timer/expiration/index.ts +3 -0
- package/src/timer/expiration/min-heap.ts +208 -0
- package/src/timer/expiration/remaining-manager.ts +241 -0
- package/src/timer/index.ts +1 -0
- package/src/type/README.md +54 -307
- package/src/type/class.ts +2 -2
- package/src/type/index.ts +14 -14
- package/src/type/is.ts +265 -2
- package/src/type/object.ts +37 -0
- package/src/type/string.ts +7 -2
- package/src/type/tuple.ts +6 -6
- package/src/type/union.ts +16 -0
- package/src/web/README.md +77 -0
- package/src/web/capture.ts +35 -0
- package/src/web/clipboard.ts +97 -0
- package/src/web/dom.ts +117 -0
- package/src/web/download.ts +16 -0
- package/src/web/event.ts +46 -0
- package/src/web/index.ts +10 -0
- package/src/web/local-storage.ts +113 -0
- package/src/web/location.ts +28 -0
- package/src/web/permission.ts +172 -0
- package/src/web/script-loader.ts +432 -0
- package/tests/unit/abort/abort-manager.spec.ts +225 -0
- package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
- package/tests/unit/basic/array.spec.ts +1 -1
- package/tests/unit/basic/object.spec.ts +32 -1
- package/tests/unit/basic/stream.spec.ts +1 -1
- package/tests/unit/basic/string.spec.ts +0 -9
- package/tests/unit/color/rgb/analyze.spec.ts +110 -0
- package/tests/unit/color/rgb/construct.spec.ts +56 -0
- package/tests/unit/color/rgb/convert.spec.ts +60 -0
- package/tests/unit/color/rgb/derive.spec.ts +103 -0
- package/tests/unit/color/rgb/parse.spec.ts +66 -0
- package/tests/unit/color/rgb/serialize.spec.ts +46 -0
- package/tests/unit/color/xyz/analyze.spec.ts +33 -0
- package/tests/unit/color/xyz/construct.spec.ts +10 -0
- package/tests/unit/color/xyz/convert.spec.ts +18 -0
- package/tests/unit/css/class.spec.ts +157 -0
- package/tests/unit/encoding/base64.spec.ts +40 -0
- package/tests/unit/environment/basic.spec.ts +20 -0
- package/tests/unit/environment/device.spec.ts +146 -0
- package/tests/unit/environment/feature.spec.ts +388 -0
- package/tests/unit/environment/geo.spec.ts +111 -0
- package/tests/unit/environment/runtime.spec.ts +364 -0
- package/tests/unit/environment/snapshot.spec.ts +4 -0
- package/tests/unit/environment/variable.spec.ts +190 -0
- package/tests/unit/event/class-event-proxy.spec.ts +225 -0
- package/tests/unit/event/event-manager.spec.ts +246 -0
- package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
- package/tests/unit/exception/browser.spec.ts +213 -0
- package/tests/unit/exception/nodejs.spec.ts +144 -0
- package/tests/unit/exception/normalize.spec.ts +57 -0
- package/tests/unit/identifier/id.spec.ts +71 -0
- package/tests/unit/identifier/uuid.spec.ts +85 -0
- package/tests/unit/log/log-emitter.spec.ts +33 -0
- package/tests/unit/log/log-scheduler.spec.ts +40 -0
- package/tests/unit/log/log-type.spec.ts +7 -0
- package/tests/unit/log/logger.spec.ts +222 -0
- package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
- package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
- package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
- package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
- package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
- package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
- package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
- package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
- package/tests/unit/random/string.spec.ts +11 -0
- package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
- package/tests/unit/reactor/preact-signal.spec.ts +1 -2
- package/tests/unit/singleton/singleton.spec.ts +49 -0
- package/tests/unit/storage/table.spec.ts +620 -0
- package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
- package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
- package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
- package/.oxlintrc.json +0 -5
package/src/type/union.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type StrictExclude<T, U extends T> = Exclude<T, U>
|
|
|
16
16
|
// ============================================================================
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
+
* Get the shared members of two union types.
|
|
20
|
+
*
|
|
19
21
|
* @example
|
|
20
22
|
* ```
|
|
21
23
|
* // Expect: "2" | "3"
|
|
@@ -28,6 +30,8 @@ export type StrictExclude<T, U extends T> = Exclude<T, U>
|
|
|
28
30
|
export type UnionIntersection<UnionA, UnionB> = UnionA extends UnionB ? UnionA : never
|
|
29
31
|
|
|
30
32
|
/**
|
|
33
|
+
* Get the members that exist in the first union but not in the second.
|
|
34
|
+
*
|
|
31
35
|
* @example
|
|
32
36
|
* ```
|
|
33
37
|
* // Expect: "1"
|
|
@@ -40,6 +44,8 @@ export type UnionIntersection<UnionA, UnionB> = UnionA extends UnionB ? UnionA :
|
|
|
40
44
|
export type UnionDifference<UnionA, UnionB> = UnionA extends UnionB ? never : UnionA
|
|
41
45
|
|
|
42
46
|
/**
|
|
47
|
+
* Get the complement of a sub-union within a larger union.
|
|
48
|
+
*
|
|
43
49
|
* @example
|
|
44
50
|
* ```
|
|
45
51
|
* // Expect: "1"
|
|
@@ -49,6 +55,8 @@ export type UnionDifference<UnionA, UnionB> = UnionA extends UnionB ? never : Un
|
|
|
49
55
|
export type UnionComplement<Union, SubUnion extends Union> = UnionDifference<Union, SubUnion>
|
|
50
56
|
|
|
51
57
|
/**
|
|
58
|
+
* Get the members that belong to exactly one of the two unions.
|
|
59
|
+
*
|
|
52
60
|
* @example
|
|
53
61
|
* ```
|
|
54
62
|
* // Expect: "1" | "4"
|
|
@@ -58,6 +66,8 @@ export type UnionComplement<Union, SubUnion extends Union> = UnionDifference<Uni
|
|
|
58
66
|
export type UnionSymmetricDifference<UnionA, UnionB> = UnionDifference<UnionA | UnionB, UnionA & UnionB>
|
|
59
67
|
|
|
60
68
|
/**
|
|
69
|
+
* Get one stable trailing member from a union.
|
|
70
|
+
*
|
|
61
71
|
* @example
|
|
62
72
|
* ```
|
|
63
73
|
* // Expect: "c"
|
|
@@ -69,6 +79,8 @@ export type LastOfUnion<Union> = UnionToIntersection<
|
|
|
69
79
|
> extends (x: infer L) => void ? L : never
|
|
70
80
|
|
|
71
81
|
/**
|
|
82
|
+
* Remove one trailing member from a union.
|
|
83
|
+
*
|
|
72
84
|
* @example
|
|
73
85
|
* ```
|
|
74
86
|
* // Expect: "a" | "b"
|
|
@@ -82,6 +94,8 @@ export type UnionPop<U> = Exclude<U, LastOfUnion<U>>
|
|
|
82
94
|
// ============================================================================
|
|
83
95
|
|
|
84
96
|
/**
|
|
97
|
+
* Convert a union type to an intersection type.
|
|
98
|
+
*
|
|
85
99
|
* @example
|
|
86
100
|
* ```
|
|
87
101
|
* // Expect: { name: string } & { age: number } & { visible: boolean }
|
|
@@ -99,6 +113,8 @@ type InternalUnionToTuple<U, T extends unknown[]> =
|
|
|
99
113
|
? T
|
|
100
114
|
: InternalUnionToTuple<Exclude<U, LastOfUnion<U>>, [LastOfUnion<U>, ...T]>
|
|
101
115
|
/**
|
|
116
|
+
* Convert a union type to a tuple type.
|
|
117
|
+
*
|
|
102
118
|
* @example
|
|
103
119
|
* ```
|
|
104
120
|
* // Expect: ["1", "2", "3"]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Web
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Web 模块用于提供面向浏览器环境(browser environment)的通用基础能力,主要承载位置、事件、文档对象模型(DOM, Document Object Model)交互、权限、剪贴板、下载、截图以及脚本加载等稳定语义。
|
|
6
|
+
|
|
7
|
+
它关注的是浏览器原生能力的通用抽象,而不是某个页面、某个框架或某个业务流程的局部实现。这个模块的价值在于把常见 Web 行为整理成边界清楚、可长期复用的公共模型,使调用方能够在不同页面和不同运行环境中复用同一套浏览器语义,而不是重复编写零散的 DOM 操作和宿主 API 调用。
|
|
8
|
+
|
|
9
|
+
## For Understanding
|
|
10
|
+
|
|
11
|
+
理解 Web 模块时,应先把它看作浏览器语义层,而不是页面逻辑层。它解决的是“浏览器和文档环境本身能提供哪些稳定能力,以及这些能力如何以较统一的方式被调用”这一类问题,因此适合放在前端应用、嵌入式 Web 视图或具备 DOM 能力的宿主环境边界中。
|
|
12
|
+
|
|
13
|
+
也因此,本模块不应承担业务页面逻辑、组件状态编排、具体框架生命周期或某一站点专用规则。适合进入这里的,应该是对位置跳转、事件监听、文档读写、脚本注入与脚本发现、资源下载、权限查询等浏览器行为的稳定抽象。只在某个业务页面中成立的选择器约定、交互流程或样式耦合逻辑,不应进入 Web 模块。
|
|
14
|
+
|
|
15
|
+
这个模块还天然带有环境前提。很多能力依赖浏览器 API、用户授权、文档对象存在与否、加载时机以及宿主兼容性。文档的职责不是掩盖这些前提,而是帮助调用方理解:Web 模块提供的是通用浏览器语义封装,环境差异、兼容策略和降级处理仍应由接入方根据实际运行条件负责。
|
|
16
|
+
|
|
17
|
+
## For Using
|
|
18
|
+
|
|
19
|
+
当你希望以更统一的方式访问浏览器能力,而不是把位置跳转、DOM 交互、事件订阅、脚本装载、权限调用等逻辑零散地散布在业务代码中时,可以使用这个模块。它适合那些需要跨页面、跨组件、跨项目复用浏览器基础语义的场景。
|
|
20
|
+
|
|
21
|
+
从使用角度看,可以把这个模块理解为若干能力类别的集合。它既包含围绕页面地址、文档标题、DOM 节点和事件订阅的基础交互能力,也包含围绕剪贴板、权限、下载与脚本处理的宿主能力封装。调用时应优先从“当前问题属于哪类浏览器语义”来理解模块,而不是直接从某个具体函数名反推设计意图。
|
|
22
|
+
|
|
23
|
+
更合理的使用方式,是把这些能力放在浏览器边界附近,先由 Web 模块完成对宿主 API 的基础整理,再由上层根据业务需要组合使用。这样既能减少页面层的重复代码,也能把兼容性检查、失败分支与资源清理等问题集中到更容易维护的位置。
|
|
24
|
+
|
|
25
|
+
## For Contributing
|
|
26
|
+
|
|
27
|
+
贡献 Web 模块时,应首先确认新增能力表达的是稳定、可复用的浏览器语义,而不是一次性的页面技巧。公共 API 的重点应是让调用方清楚理解“这里封装的是哪一种浏览器行为、适用于什么边界、失败时大致是什么语义”,而不是让模块变成存放各种 DOM 小技巧的收纳盒。
|
|
28
|
+
|
|
29
|
+
继续扩展时,应特别注意两个方向。其一,不要把业务页面逻辑、站点特定约定或框架专属行为混入 Web 模块。其二,不要为了追求表面简洁而隐藏重要的环境前提,例如用户授权、脚本加载顺序、文档可用性或资源清理责任。Web 模块可以帮助收敛这些问题,但不应伪装成“在任何环境中都无条件可靠”的黑箱。
|
|
30
|
+
|
|
31
|
+
### JSDoc 注释格式要求
|
|
32
|
+
|
|
33
|
+
- 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让人在不跳转实现的情况下就能理解用途。
|
|
34
|
+
- JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
|
|
35
|
+
- 如果描述后还有其他内容,应在描述后加一个空行。
|
|
36
|
+
- 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
|
|
37
|
+
- 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
|
|
38
|
+
- 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
|
|
39
|
+
- 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
|
|
40
|
+
- 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
|
|
41
|
+
- 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
|
|
42
|
+
- 如果函数返回结构化字符串,应展示其预期格式特征。
|
|
43
|
+
- 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
|
|
44
|
+
|
|
45
|
+
### 实现规范要求
|
|
46
|
+
|
|
47
|
+
- 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
|
|
48
|
+
- 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
|
|
49
|
+
- 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
|
|
50
|
+
- 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
|
|
51
|
+
- 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
|
|
52
|
+
- 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
|
|
53
|
+
- 辅助元素永远不要公开导出。
|
|
54
|
+
- 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/web/internal.ts`。
|
|
55
|
+
- 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
|
|
56
|
+
- 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
|
|
57
|
+
- 子模块不需要有自己的 `README.md`。
|
|
58
|
+
- 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
|
|
59
|
+
- 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
|
|
60
|
+
- 与浏览器交互不可避免地会产生副作用,但应尽量把副作用边界限制在公开能力内部,并让返回值、错误语义、订阅语义与清理语义保持稳定。
|
|
61
|
+
|
|
62
|
+
### 导出策略要求
|
|
63
|
+
|
|
64
|
+
- 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
|
|
65
|
+
- 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
|
|
66
|
+
- Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
|
|
67
|
+
- 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的浏览器语义,而不是某段实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
|
|
68
|
+
|
|
69
|
+
### 测试要求
|
|
70
|
+
|
|
71
|
+
- 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
|
|
72
|
+
- 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
|
|
73
|
+
- 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
|
|
74
|
+
- 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
|
|
75
|
+
- 测试顺序应与源文件中被测试目标的原始顺序保持一致。
|
|
76
|
+
- 模块的单元测试文件目录是 `./tests/unit/web`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/web/<sub-module-name>`。
|
|
77
|
+
- 此模块暂时不需要任何测试,因为它主要是对浏览器能力的封装,且这些能力本身已经由浏览器厂商维护测试覆盖。未来如果需要在模块内添加更复杂的逻辑或状态管理时,可以再考虑增加针对这些逻辑的单元测试。另外一个原因是,浏览器环境的测试通常需要特定的运行环境和工具链支持,开发和维护成本较高,因此在没有明确需求的情况下,先保持模块的纯粹封装性质也是合理的。
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { toJpeg, toPng } from "html-to-image"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 表示将 DOM 节点导出为图像时可配置的选项。
|
|
5
|
+
*/
|
|
6
|
+
export interface DomToImageOptions {
|
|
7
|
+
node: HTMLElement
|
|
8
|
+
imageType?: "jpeg" | "png" | undefined
|
|
9
|
+
extraOptions?: Exclude<Parameters<typeof toJpeg>[1], undefined>
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Convert a DOM node to an image (JPEG or PNG) and return the image as a data URL.
|
|
13
|
+
* The image type can be specified in the options, and additional options for the
|
|
14
|
+
* conversion can also be provided.
|
|
15
|
+
*/
|
|
16
|
+
export const domToImage = async (options: DomToImageOptions): Promise<string | undefined> => {
|
|
17
|
+
const { node, imageType, extraOptions } = options
|
|
18
|
+
const preparedOptions = { cacheBust: true, ...extraOptions }
|
|
19
|
+
const toImage = imageType === "jpeg" ? toJpeg : toPng
|
|
20
|
+
const image = await toImage(node, preparedOptions)
|
|
21
|
+
return image
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert a canvas element to a Blob object.
|
|
26
|
+
*/
|
|
27
|
+
export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise<Blob | null> => {
|
|
28
|
+
const blob = await new Promise<Blob | null>((resolve) => {
|
|
29
|
+
canvas.toBlob((blob) => {
|
|
30
|
+
resolve(blob)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return blob
|
|
35
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ensureClipboard } from "#Source/environment/index.ts"
|
|
2
|
+
import { ensurePermissionGranted } from "./permission.ts"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Writes the given text to the clipboard. This function ensures
|
|
6
|
+
* that the Clipboard API is available and that the necessary
|
|
7
|
+
* permissions are granted before attempting to write to the clipboard.
|
|
8
|
+
*/
|
|
9
|
+
export const writeTextToClipboard = async <T extends string>(text: T): Promise<T> => {
|
|
10
|
+
ensureClipboard()
|
|
11
|
+
await ensurePermissionGranted({ name: "clipboard-write" })
|
|
12
|
+
|
|
13
|
+
// write text to clipboard
|
|
14
|
+
await navigator.clipboard.writeText(text)
|
|
15
|
+
|
|
16
|
+
return text
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reads text from the clipboard. This function ensures that
|
|
21
|
+
* the Clipboard API is available and that the necessary
|
|
22
|
+
* permissions are granted before attempting to read from the clipboard.
|
|
23
|
+
*
|
|
24
|
+
* It returns the text read from the clipboard as a string.
|
|
25
|
+
*/
|
|
26
|
+
export const readTextFromClipboard = async (): Promise<string> => {
|
|
27
|
+
ensureClipboard()
|
|
28
|
+
await ensurePermissionGranted({ name: "clipboard-read" })
|
|
29
|
+
|
|
30
|
+
// read text from clipboard
|
|
31
|
+
const text = await navigator.clipboard.readText()
|
|
32
|
+
|
|
33
|
+
return text
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Writes the given image blob to the clipboard. This function ensures
|
|
38
|
+
* that the Clipboard API is available and that the necessary
|
|
39
|
+
* permissions are granted before attempting to write to the clipboard.
|
|
40
|
+
*
|
|
41
|
+
* It returns the blob that was written to the clipboard.
|
|
42
|
+
*/
|
|
43
|
+
export const writeBlobImageToClipboard = async <T extends Blob>(
|
|
44
|
+
blob: T
|
|
45
|
+
): Promise<T> => {
|
|
46
|
+
ensureClipboard()
|
|
47
|
+
await ensurePermissionGranted({ name: "clipboard-write" })
|
|
48
|
+
|
|
49
|
+
// write image to clipboard
|
|
50
|
+
await navigator.clipboard.write([
|
|
51
|
+
new ClipboardItem({ [blob.type]: blob }),
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
return blob
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const base64ToBlob = (base64: string): Blob | null => {
|
|
58
|
+
try {
|
|
59
|
+
const byteString = atob(base64.split(",")[1] ?? "")
|
|
60
|
+
const mimeString = base64.split(",")[0]?.split(":")[1]?.split(";")[0]
|
|
61
|
+
if (mimeString === undefined) {
|
|
62
|
+
throw new Error("Invalid MIME type")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const arrayBuffer = new ArrayBuffer(byteString.length)
|
|
66
|
+
const uintArray = new Uint8Array(arrayBuffer)
|
|
67
|
+
for (let i = 0; i < byteString.length; i = i + 1) {
|
|
68
|
+
const codePoint = byteString.codePointAt(i)
|
|
69
|
+
if (codePoint === undefined) {
|
|
70
|
+
throw new Error("Invalid character in base64 string")
|
|
71
|
+
}
|
|
72
|
+
uintArray[i] = codePoint
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return new Blob([arrayBuffer], { type: mimeString })
|
|
76
|
+
} catch (exception) {
|
|
77
|
+
console.error("Failed to convert base64 to Blob:", exception)
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Writes a base64-encoded image to the clipboard. This function first converts
|
|
83
|
+
* the base64 string to a Blob object and then uses the `writeBlobImageToClipboard`
|
|
84
|
+
* function to write the image to the clipboard.
|
|
85
|
+
*
|
|
86
|
+
* It returns the original base64 string if the operation is successful.
|
|
87
|
+
*/
|
|
88
|
+
export const writeBase64ImageToClipboard = async <T extends string>(
|
|
89
|
+
base64: T
|
|
90
|
+
): Promise<T> => {
|
|
91
|
+
const blob = base64ToBlob(base64)
|
|
92
|
+
if (blob === null) {
|
|
93
|
+
throw new Error("Failed to convert base64 to Blob")
|
|
94
|
+
}
|
|
95
|
+
await writeBlobImageToClipboard(blob)
|
|
96
|
+
return base64
|
|
97
|
+
}
|
package/src/web/dom.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* 获取当前文档标题。
|
|
4
|
+
*/
|
|
5
|
+
export const getDocumentTitle = (): string => {
|
|
6
|
+
return document.title
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 设置当前文档标题。
|
|
11
|
+
*/
|
|
12
|
+
export const setDocumentTitle = (title: string): void => {
|
|
13
|
+
document.title = title
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 重置输入元素的值,常用于清空文件输入框。
|
|
18
|
+
*/
|
|
19
|
+
export const resetInput = (input: HTMLInputElement | null | undefined): void => {
|
|
20
|
+
if (input === null || input === undefined) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
input.value = ""
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Prevent double click selection, but allow drag selection.
|
|
28
|
+
*
|
|
29
|
+
* @see {@link https://www.cnblogs.com/walkermag/p/16700244.html}
|
|
30
|
+
*/
|
|
31
|
+
export const disableDoubleClickSelection = (event: MouseEvent): void => {
|
|
32
|
+
if (event.detail > 1) {
|
|
33
|
+
event.preventDefault()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Listen for clicks outside of the specified node, and execute
|
|
39
|
+
* a callback when such clicks occur.
|
|
40
|
+
*/
|
|
41
|
+
export const onClickOutside = (
|
|
42
|
+
node: HTMLElement | null,
|
|
43
|
+
callback: () => void
|
|
44
|
+
): (() => void) => {
|
|
45
|
+
const handleClick = (event: MouseEvent): void => {
|
|
46
|
+
if (event.target === null) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
50
|
+
if (node !== null && node.contains(event.target as Node) === false) {
|
|
51
|
+
callback()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
document.addEventListener("click", handleClick)
|
|
55
|
+
return () => {
|
|
56
|
+
document.removeEventListener("click", handleClick)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 表示注入脚本节点时可配置的选项。
|
|
62
|
+
*/
|
|
63
|
+
export interface InjectScriptOptions {
|
|
64
|
+
src: string
|
|
65
|
+
onload?: NonNullable<GlobalEventHandlers['onload']> | undefined
|
|
66
|
+
removeAfterLoaded?: boolean | undefined
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Inject a script into the document head.
|
|
70
|
+
*/
|
|
71
|
+
export const injectScript = (options: InjectScriptOptions): HTMLScriptElement => {
|
|
72
|
+
const { src, onload, removeAfterLoaded } = options
|
|
73
|
+
|
|
74
|
+
const script = document.createElement('script')
|
|
75
|
+
script.setAttribute('type', 'text/javascript')
|
|
76
|
+
script.src = src
|
|
77
|
+
script.addEventListener('load', (...args): void => {
|
|
78
|
+
if (onload !== undefined) {
|
|
79
|
+
onload.bind(script)(...args)
|
|
80
|
+
}
|
|
81
|
+
if (removeAfterLoaded === true) {
|
|
82
|
+
script.remove()
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
document.head.append(script)
|
|
86
|
+
|
|
87
|
+
return script
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 表示轮询查找 DOM 元素时可配置的选项。
|
|
92
|
+
*/
|
|
93
|
+
export interface PollingToGetElementOptions {
|
|
94
|
+
selector: string
|
|
95
|
+
interval: number
|
|
96
|
+
callback: (node: Element) => void
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Poll the DOM to get an element by selector, and execute a callback
|
|
100
|
+
* when the element is found.
|
|
101
|
+
*/
|
|
102
|
+
export const pollingToGetElement = (options: PollingToGetElementOptions): void => {
|
|
103
|
+
const { selector, interval, callback } = options
|
|
104
|
+
|
|
105
|
+
let node: Element | null = null
|
|
106
|
+
const fixedSelector = (selector.includes('#') || selector.includes('.'))
|
|
107
|
+
? selector
|
|
108
|
+
: `#${selector}`
|
|
109
|
+
node = document.querySelector(fixedSelector)
|
|
110
|
+
const timer = setInterval(() => {
|
|
111
|
+
node = node ?? document.querySelector(fixedSelector)
|
|
112
|
+
if (node !== null) {
|
|
113
|
+
clearInterval(timer)
|
|
114
|
+
callback(node)
|
|
115
|
+
}
|
|
116
|
+
}, interval)
|
|
117
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Download a base64 image as a file with the specified filename.
|
|
4
|
+
*/
|
|
5
|
+
export const downloadImage = (base64: string, filename: string): void => {
|
|
6
|
+
const element = document.createElement("a")
|
|
7
|
+
element.setAttribute("href", base64)
|
|
8
|
+
element.setAttribute("download", filename)
|
|
9
|
+
|
|
10
|
+
element.style.display = "none"
|
|
11
|
+
document.body.append(element)
|
|
12
|
+
|
|
13
|
+
element.click()
|
|
14
|
+
|
|
15
|
+
element.remove()
|
|
16
|
+
}
|
package/src/web/event.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Listen for page hide events, and execute a callback when
|
|
3
|
+
* such events occur.
|
|
4
|
+
*
|
|
5
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event}
|
|
6
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event}
|
|
7
|
+
*/
|
|
8
|
+
export const onPageHide = (callback: (event: PageTransitionEvent) => void): void => {
|
|
9
|
+
window.addEventListener("pagehide", (event: PageTransitionEvent): void => {
|
|
10
|
+
callback(event)
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Listen for visibility change events, and execute a callback when
|
|
16
|
+
* such events occur.
|
|
17
|
+
*
|
|
18
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event}
|
|
19
|
+
*/
|
|
20
|
+
export const onVisibilityChange = (callback: (visibilityState: DocumentVisibilityState) => void): void => {
|
|
21
|
+
window.addEventListener("visibilitychange", (): void => {
|
|
22
|
+
callback(document.visibilityState)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Listen for visibility change events, and execute a callback when
|
|
27
|
+
* the page becomes hidden.
|
|
28
|
+
*/
|
|
29
|
+
export const onVisibilityChangeToHidden = (callback: () => void): void => {
|
|
30
|
+
onVisibilityChange((visibilityState) => {
|
|
31
|
+
if (visibilityState === "hidden") {
|
|
32
|
+
callback()
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Listen for visibility change events, and execute a callback when
|
|
38
|
+
* the page becomes visible.
|
|
39
|
+
*/
|
|
40
|
+
export const onVisibilityChangeToVisible = (callback: () => void): void => {
|
|
41
|
+
onVisibilityChange((visibilityState) => {
|
|
42
|
+
if (visibilityState === "visible") {
|
|
43
|
+
callback()
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
package/src/web/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./location.ts"
|
|
2
|
+
export * from "./event.ts"
|
|
3
|
+
export * from "./local-storage.ts"
|
|
4
|
+
export * from "./dom.ts"
|
|
5
|
+
export * from "./permission.ts"
|
|
6
|
+
export * from "./clipboard.ts"
|
|
7
|
+
|
|
8
|
+
export * from "./capture.ts"
|
|
9
|
+
export * from "./download.ts"
|
|
10
|
+
export * from "./script-loader.ts"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Get the number of key-value pairs currently stored in local storage.
|
|
4
|
+
*/
|
|
5
|
+
export const getLengthOfLocalStorage = (): number => {
|
|
6
|
+
return localStorage.length
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get an array of all keys currently stored in local storage.
|
|
11
|
+
*/
|
|
12
|
+
export const getAllKeysFromLocalStorage = (): string[] => {
|
|
13
|
+
const keys: string[] = []
|
|
14
|
+
for (let i = 0; i < localStorage.length; i = i + 1) {
|
|
15
|
+
const key = localStorage.key(i)
|
|
16
|
+
if (key !== null) {
|
|
17
|
+
keys.push(key)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return keys
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if a specific key exists in local storage.
|
|
25
|
+
*/
|
|
26
|
+
export const hasKeyInLocalStorage = (key: string): boolean => {
|
|
27
|
+
return localStorage.getItem(key) !== null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Retrieve a value from local storage by its key.
|
|
32
|
+
*/
|
|
33
|
+
export const getFromLocalStorage = (key: string): string | null => {
|
|
34
|
+
return localStorage.getItem(key)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set a value in local storage by its key.
|
|
39
|
+
* If the value is null, the key will be removed from local storage.
|
|
40
|
+
*/
|
|
41
|
+
export const setToLocalStorage = (key: string, value: string | null): string | null => {
|
|
42
|
+
if (value !== null) {
|
|
43
|
+
localStorage.setItem(key, value)
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
localStorage.removeItem(key)
|
|
47
|
+
}
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Remove a value from local storage by its key.
|
|
53
|
+
* Returns the removed value, or null if the key did not exist.
|
|
54
|
+
*/
|
|
55
|
+
export const removeFromLocalStorage = (key: string): string | null => {
|
|
56
|
+
const value = localStorage.getItem(key)
|
|
57
|
+
localStorage.removeItem(key)
|
|
58
|
+
return value
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clear all key-value pairs from local storage.
|
|
63
|
+
*/
|
|
64
|
+
export const clearLocalStorage = (): void => {
|
|
65
|
+
localStorage.clear()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Subscribe to changes in local storage. The callback will be
|
|
70
|
+
* invoked whenever any key-value pair in local storage changes.
|
|
71
|
+
* Returns a function to unsubscribe from the changes.
|
|
72
|
+
*/
|
|
73
|
+
export const subscribeAllChangesOfLocalStorage = (
|
|
74
|
+
subscriber: (event: StorageEvent) => void
|
|
75
|
+
): (() => void) => {
|
|
76
|
+
const internalSubscriber = (event: StorageEvent): void => {
|
|
77
|
+
subscriber(event)
|
|
78
|
+
}
|
|
79
|
+
const subscribe = (): void => {
|
|
80
|
+
window.addEventListener("storage", internalSubscriber)
|
|
81
|
+
}
|
|
82
|
+
const unsubscribe = (): void => {
|
|
83
|
+
window.removeEventListener("storage", internalSubscriber)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
subscribe()
|
|
87
|
+
return unsubscribe
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Subscribe to changes in a specific key in local storage.
|
|
92
|
+
* The callback will be invoked whenever the value of the key changes.
|
|
93
|
+
* Returns a function to unsubscribe from the changes.
|
|
94
|
+
*/
|
|
95
|
+
export const subscribeKeyChangesOfLocalStorage = (
|
|
96
|
+
key: string,
|
|
97
|
+
subscriber: (event: StorageEvent) => void
|
|
98
|
+
): (() => void) => {
|
|
99
|
+
const internalSubscriber = (event: StorageEvent): void => {
|
|
100
|
+
if (event.key === key) {
|
|
101
|
+
subscriber(event)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const subscribe = (): void => {
|
|
105
|
+
window.addEventListener("storage", internalSubscriber)
|
|
106
|
+
}
|
|
107
|
+
const unsubscribe = (): void => {
|
|
108
|
+
window.removeEventListener("storage", internalSubscriber)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
subscribe()
|
|
112
|
+
return unsubscribe
|
|
113
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* 获取当前页面的来源(origin)。
|
|
4
|
+
*/
|
|
5
|
+
export const getOrigin = (): string => {
|
|
6
|
+
return window.location.origin
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 获取当前页面的完整地址。
|
|
11
|
+
*/
|
|
12
|
+
export const getHref = (): string => {
|
|
13
|
+
return window.location.href
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 跳转到指定地址,并向历史记录中新增一条记录。
|
|
18
|
+
*/
|
|
19
|
+
export const navigateTo = (url: string): void => {
|
|
20
|
+
window.location.assign(url)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 跳转到指定地址,并替换当前历史记录。
|
|
25
|
+
*/
|
|
26
|
+
export const redirectTo = (url: string): void => {
|
|
27
|
+
window.location.replace(url)
|
|
28
|
+
}
|