@lytjs/common-dom-helpers 6.5.0 → 6.7.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/README.md CHANGED
@@ -1,127 +1,127 @@
1
- # @lytjs/common-dom-helpers
2
-
3
- 轻量级 DOM 操作辅助工具,提供常用的 DOM 创建、修改和查询方法。
4
-
5
- ## 安装
6
-
7
- ```bash
8
- pnpm add @lytjs/common-dom-helpers
9
- ```
10
-
11
- ## API
12
-
13
- ### `createElement(tag, attrs?, children?): Element`
14
-
15
- 创建 DOM 元素,支持设置属性和子元素。
16
-
17
- ```typescript
18
- import { createElement } from '@lytjs/common-dom-helpers';
19
-
20
- const el = createElement('div', { id: 'app', class: 'container' }, [
21
- 'Hello',
22
- createElement('span', {}, ['World']),
23
- ]);
24
- ```
25
-
26
- ### `insertBefore(parent, child, ref): void`
27
-
28
- 在参考节点前插入子节点,ref 为 null 时等同于 appendChild。
29
-
30
- ```typescript
31
- import { insertBefore } from '@lytjs/common-dom-helpers';
32
-
33
- insertBefore(parent, newChild, refChild);
34
- insertBefore(parent, newChild, null); // appendChild
35
- ```
36
-
37
- ### `removeChild(parent, child): boolean`
38
-
39
- 移除子节点,成功返回 true,失败返回 false。
40
-
41
- ```typescript
42
- import { removeChild } from '@lytjs/common-dom-helpers';
43
-
44
- const success = removeChild(parent, child); // true or false
45
- ```
46
-
47
- ### `nextSibling(node, skipComments?): Node | null`
48
-
49
- 获取下一个兄弟节点,可通过参数控制是否跳过注释节点。
50
-
51
- ```typescript
52
- import { nextSibling } from '@lytjs/common-dom-helpers';
53
-
54
- const next = nextSibling(node); // includes comments
55
- const next = nextSibling(node, true); // skips comments
56
- ```
57
-
58
- ### `createTextNode(text): Text`
59
-
60
- 创建文本节点。
61
-
62
- ```typescript
63
- import { createTextNode } from '@lytjs/common-dom-helpers';
64
-
65
- const text = createTextNode('Hello World');
66
- ```
67
-
68
- ### `createComment(text): Comment`
69
-
70
- 创建注释节点。
71
-
72
- ```typescript
73
- import { createComment } from '@lytjs/common-dom-helpers';
74
-
75
- const comment = createComment('this is a comment');
76
- ```
77
-
78
- ### `setStyle(el, styles): void`
79
-
80
- 批量设置元素样式。
81
-
82
- ```typescript
83
- import { setStyle } from '@lytjs/common-dom-helpers';
84
-
85
- setStyle(el, { color: 'red', fontSize: '16px', zIndex: 10 });
86
- ```
87
-
88
- ### `hasClass(el, cls): boolean`
89
-
90
- 检查元素是否有指定 class。
91
-
92
- ```typescript
93
- import { hasClass } from '@lytjs/common-dom-helpers';
94
-
95
- hasClass(el, 'active'); // true or false
96
- ```
97
-
98
- ### `addClass(el, ...cls): void`
99
-
100
- 添加 class。
101
-
102
- ```typescript
103
- import { addClass } from '@lytjs/common-dom-helpers';
104
-
105
- addClass(el, 'active', 'visible');
106
- ```
107
-
108
- ### `removeClass(el, ...cls): void`
109
-
110
- 移除 class。
111
-
112
- ```typescript
113
- import { removeClass } from '@lytjs/common-dom-helpers';
114
-
115
- removeClass(el, 'active', 'hidden');
116
- ```
117
-
118
- ## 特性
119
-
120
- - 零运行时依赖
121
- - 体积 < 3KB(min+gzip)
122
- - Node.js 环境安全(非浏览器环境不会崩溃)
123
- - TypeScript 类型完整
124
-
125
- ## License
126
-
127
- MIT
1
+ # @lytjs/common-dom-helpers
2
+
3
+ 轻量级 DOM 操作辅助工具,提供常用的 DOM 创建、修改和查询方法。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add @lytjs/common-dom-helpers
9
+ ```
10
+
11
+ ## API
12
+
13
+ ### `createElement(tag, attrs?, children?): Element`
14
+
15
+ 创建 DOM 元素,支持设置属性和子元素。
16
+
17
+ ```typescript
18
+ import { createElement } from '@lytjs/common-dom-helpers';
19
+
20
+ const el = createElement('div', { id: 'app', class: 'container' }, [
21
+ 'Hello',
22
+ createElement('span', {}, ['World']),
23
+ ]);
24
+ ```
25
+
26
+ ### `insertBefore(parent, child, ref): void`
27
+
28
+ 在参考节点前插入子节点,ref 为 null 时等同于 appendChild。
29
+
30
+ ```typescript
31
+ import { insertBefore } from '@lytjs/common-dom-helpers';
32
+
33
+ insertBefore(parent, newChild, refChild);
34
+ insertBefore(parent, newChild, null); // appendChild
35
+ ```
36
+
37
+ ### `removeChild(parent, child): boolean`
38
+
39
+ 移除子节点,成功返回 true,失败返回 false。
40
+
41
+ ```typescript
42
+ import { removeChild } from '@lytjs/common-dom-helpers';
43
+
44
+ const success = removeChild(parent, child); // true or false
45
+ ```
46
+
47
+ ### `nextSibling(node, skipComments?): Node | null`
48
+
49
+ 获取下一个兄弟节点,可通过参数控制是否跳过注释节点。
50
+
51
+ ```typescript
52
+ import { nextSibling } from '@lytjs/common-dom-helpers';
53
+
54
+ const next = nextSibling(node); // includes comments
55
+ const next = nextSibling(node, true); // skips comments
56
+ ```
57
+
58
+ ### `createTextNode(text): Text`
59
+
60
+ 创建文本节点。
61
+
62
+ ```typescript
63
+ import { createTextNode } from '@lytjs/common-dom-helpers';
64
+
65
+ const text = createTextNode('Hello World');
66
+ ```
67
+
68
+ ### `createComment(text): Comment`
69
+
70
+ 创建注释节点。
71
+
72
+ ```typescript
73
+ import { createComment } from '@lytjs/common-dom-helpers';
74
+
75
+ const comment = createComment('this is a comment');
76
+ ```
77
+
78
+ ### `setStyle(el, styles): void`
79
+
80
+ 批量设置元素样式。
81
+
82
+ ```typescript
83
+ import { setStyle } from '@lytjs/common-dom-helpers';
84
+
85
+ setStyle(el, { color: 'red', fontSize: '16px', zIndex: 10 });
86
+ ```
87
+
88
+ ### `hasClass(el, cls): boolean`
89
+
90
+ 检查元素是否有指定 class。
91
+
92
+ ```typescript
93
+ import { hasClass } from '@lytjs/common-dom-helpers';
94
+
95
+ hasClass(el, 'active'); // true or false
96
+ ```
97
+
98
+ ### `addClass(el, ...cls): void`
99
+
100
+ 添加 class。
101
+
102
+ ```typescript
103
+ import { addClass } from '@lytjs/common-dom-helpers';
104
+
105
+ addClass(el, 'active', 'visible');
106
+ ```
107
+
108
+ ### `removeClass(el, ...cls): void`
109
+
110
+ 移除 class。
111
+
112
+ ```typescript
113
+ import { removeClass } from '@lytjs/common-dom-helpers';
114
+
115
+ removeClass(el, 'active', 'hidden');
116
+ ```
117
+
118
+ ## 特性
119
+
120
+ - 零运行时依赖
121
+ - 体积 < 3KB(min+gzip)
122
+ - Node.js 环境安全(非浏览器环境不会崩溃)
123
+ - TypeScript 类型完整
124
+
125
+ ## License
126
+
127
+ MIT
package/dist/index.cjs CHANGED
@@ -55,15 +55,7 @@ var DANGEROUS_EVENT_ATTRS = /* @__PURE__ */ new Set([
55
55
  "ontransitionend",
56
56
  "ontransitioncancel"
57
57
  ]);
58
- var URL_ATTRS = /* @__PURE__ */ new Set([
59
- "href",
60
- "src",
61
- "action",
62
- "formaction",
63
- "xlink:href",
64
- "data",
65
- "poster"
66
- ]);
58
+ var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "data", "poster"]);
67
59
  var SAFE_URL_PROTOCOLS = /* @__PURE__ */ new Set([
68
60
  "http:",
69
61
  "https:",
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\r\n * @lytjs/common-dom-helpers\r\n * 轻量级 DOM 操作辅助工具\r\n */\r\n\r\nconst isBrowser =\r\n typeof window !== 'undefined' &&\r\n typeof document !== 'undefined' &&\r\n typeof document.createElement === 'function';\r\n\r\n/** 危险的事件属性黑名单,防止通过属性注入事件处理器 */\r\nconst DANGEROUS_EVENT_ATTRS = new Set([\r\n 'onclick', 'ondblclick', 'onmousedown', 'onmouseup', 'onmouseover',\r\n 'onmousemove', 'onmouseout', 'onmouseenter', 'onmouseleave',\r\n 'onkeydown', 'onkeyup', 'onkeypress',\r\n 'onfocus', 'onblur', 'oninput', 'onchange', 'onsubmit', 'onreset',\r\n 'onload', 'onunload', 'onbeforeunload', 'onerror', 'onresize',\r\n 'onscroll', 'onwheel', 'ondrag', 'ondragstart', 'ondragend',\r\n 'ondragenter', 'ondragleave', 'ondragover', 'ondrop',\r\n 'oncopy', 'oncut', 'onpaste',\r\n 'oncontextmenu', 'onselect', 'onselectstart',\r\n 'ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel',\r\n 'onpointerdown', 'onpointerup', 'onpointermove',\r\n 'onanimationstart', 'onanimationend', 'onanimationiteration',\r\n 'ontransitionstart', 'ontransitionend', 'ontransitioncancel',\r\n]);\r\n\r\n/** 需要 URL 安全检查的属性 */\r\nconst URL_ATTRS = new Set([\r\n 'href', 'src', 'action', 'formaction', 'xlink:href', 'data', 'poster',\r\n]);\r\n\r\n/** 安全的 URL 协议白名单 */\r\nconst SAFE_URL_PROTOCOLS = new Set([\r\n 'http:', 'https:', 'mailto:', 'tel:', 'ftp:', '.', '/', '#', '?',\r\n]);\r\n\r\n/**\r\n * 检查属性名是否安全(非事件处理器)\r\n */\r\nfunction isSafeAttr(key: string): boolean {\r\n return !DANGEROUS_EVENT_ATTRS.has(key.toLowerCase());\r\n}\r\n\r\n/**\r\n * 检查 URL 属性值是否安全(非 javascript: 等危险协议)\r\n */\r\nfunction isSafeURL(value: string): boolean {\r\n const trimmed = value.trim().toLowerCase();\r\n // 相对 URL、hash、query 是安全的\r\n if (trimmed.startsWith('#') || trimmed.startsWith('?') || trimmed.startsWith('/') || trimmed.startsWith('.')) {\r\n return true;\r\n }\r\n // 检查协议\r\n const colonIndex = trimmed.indexOf(':');\r\n if (colonIndex > 0) {\r\n const protocol = trimmed.slice(0, colonIndex + 1);\r\n return SAFE_URL_PROTOCOLS.has(protocol);\r\n }\r\n return true;\r\n}\r\n\r\n/**\r\n * 创建 DOM 元素\r\n *\r\n * @param tag - 标签名\r\n * @param attrs - 属性对象(可选)\r\n * @param children - 子元素列表,字符串会创建 TextNode(可选)\r\n * @returns 创建的 Element\r\n */\r\nexport function createElement(\r\n tag: string,\r\n attrs?: Record<string, string>,\r\n children?: (string | Node)[],\r\n): Element {\r\n if (!isBrowser) {\r\n throw new Error('createElement is only available in browser environments');\r\n }\r\n\r\n const el = document.createElement(tag);\r\n\r\n if (attrs) {\r\n for (const key of Object.keys(attrs)) {\r\n const val = attrs[key];\r\n if (val !== undefined && isSafeAttr(key)) {\r\n // 对 URL 类属性进行协议安全检查\r\n if (URL_ATTRS.has(key.toLowerCase()) && !isSafeURL(val)) {\r\n continue; // 跳过危险的 URL 属性值\r\n }\r\n el.setAttribute(key, val);\r\n }\r\n }\r\n }\r\n\r\n if (children) {\r\n for (const child of children) {\r\n if (typeof child === 'string') {\r\n el.appendChild(document.createTextNode(child));\r\n } else {\r\n el.appendChild(child);\r\n }\r\n }\r\n }\r\n\r\n return el;\r\n}\r\n\r\n/**\r\n * 在参考节点前插入子节点\r\n *\r\n * @param parent - 父节点\r\n * @param child - 要插入的子节点\r\n * @param ref - 参考节点,为 null 时等同于 appendChild\r\n */\r\nexport function insertBefore(\r\n parent: Node,\r\n child: Node,\r\n ref: Node | null,\r\n): void {\r\n if (!isBrowser) {\r\n throw new Error('insertBefore is only available in browser environments');\r\n }\r\n\r\n parent.insertBefore(child, ref);\r\n}\r\n\r\n/**\r\n * 移除子节点\r\n *\r\n * @param parent - 父节点\r\n * @param child - 要移除的子节点\r\n * @returns 成功返回 true,失败返回 false\r\n */\r\nexport function removeChild(parent: Node, child: Node): boolean {\r\n if (!isBrowser) {\r\n return false;\r\n }\r\n\r\n try {\r\n parent.removeChild(child);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * 获取下一个兄弟节点\r\n *\r\n * @param node - 当前节点\r\n * @param skipComments - 是否跳过注释节点,默认为 false\r\n * @returns 下一个兄弟节点,如果没有则返回 null\r\n */\r\nexport function nextSibling(node: Node, skipComments: boolean = false): Node | null {\r\n if (!isBrowser) {\r\n return null;\r\n }\r\n\r\n let sibling = node.nextSibling;\r\n if (skipComments) {\r\n while (sibling && sibling.nodeType === Node.COMMENT_NODE) {\r\n sibling = sibling.nextSibling;\r\n }\r\n }\r\n return sibling;\r\n}\r\n\r\n/**\r\n * 创建文本节点\r\n *\r\n * @param text - 文本内容\r\n * @returns Text 节点\r\n */\r\nexport function createTextNode(text: string): Text {\r\n if (!isBrowser) {\r\n throw new Error('createTextNode is only available in browser environments');\r\n }\r\n\r\n return document.createTextNode(text);\r\n}\r\n\r\n/**\r\n * 创建注释节点\r\n *\r\n * @param text - 注释内容\r\n * @returns Comment 节点\r\n */\r\nexport function createComment(text: string): Comment {\r\n if (!isBrowser) {\r\n throw new Error('createComment is only available in browser environments');\r\n }\r\n\r\n return document.createComment(text);\r\n}\r\n\r\n/**\r\n * 批量设置元素样式\r\n *\r\n * @param el - 目标元素\r\n * @param styles - 样式键值对\r\n */\r\nexport function setStyle(\r\n el: Element,\r\n styles: Record<string, string | number>,\r\n): void {\r\n if (!isBrowser) {\r\n throw new Error('setStyle is only available in browser environments');\r\n }\r\n\r\n const htmlEl = el as HTMLElement;\r\n for (const key of Object.keys(styles)) {\r\n (htmlEl.style as unknown as Record<string, string>)[key] = String(styles[key]);\r\n }\r\n}\r\n\r\n/**\r\n * 检查元素是否有指定 class\r\n *\r\n * @param el - 目标元素\r\n * @param cls - class 名称\r\n * @returns 是否包含该 class\r\n */\r\nexport function hasClass(el: Element, cls: string): boolean {\r\n if (!isBrowser) {\r\n return false;\r\n }\r\n\r\n return el.classList.contains(cls);\r\n}\r\n\r\n/**\r\n * 添加 class\r\n *\r\n * @param el - 目标元素\r\n * @param cls - 要添加的 class 名称列表\r\n */\r\nexport function addClass(el: Element, ...cls: string[]): void {\r\n if (!isBrowser) {\r\n return;\r\n }\r\n\r\n el.classList.add(...cls);\r\n}\r\n\r\n/**\r\n * 移除 class\r\n *\r\n * @param el - 目标元素\r\n * @param cls - 要移除的 class 名称列表\r\n */\r\nexport function removeClass(el: Element, ...cls: string[]): void {\r\n if (!isBrowser) {\r\n return;\r\n }\r\n\r\n el.classList.remove(...cls);\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lytjs/common-dom-helpers\n * 轻量级 DOM 操作辅助工具\n */\n\nconst isBrowser =\n typeof window !== 'undefined' &&\n typeof document !== 'undefined' &&\n typeof document.createElement === 'function';\n\n/** 危险的事件属性黑名单,防止通过属性注入事件处理器 */\nconst DANGEROUS_EVENT_ATTRS = new Set([\n 'onclick',\n 'ondblclick',\n 'onmousedown',\n 'onmouseup',\n 'onmouseover',\n 'onmousemove',\n 'onmouseout',\n 'onmouseenter',\n 'onmouseleave',\n 'onkeydown',\n 'onkeyup',\n 'onkeypress',\n 'onfocus',\n 'onblur',\n 'oninput',\n 'onchange',\n 'onsubmit',\n 'onreset',\n 'onload',\n 'onunload',\n 'onbeforeunload',\n 'onerror',\n 'onresize',\n 'onscroll',\n 'onwheel',\n 'ondrag',\n 'ondragstart',\n 'ondragend',\n 'ondragenter',\n 'ondragleave',\n 'ondragover',\n 'ondrop',\n 'oncopy',\n 'oncut',\n 'onpaste',\n 'oncontextmenu',\n 'onselect',\n 'onselectstart',\n 'ontouchstart',\n 'ontouchend',\n 'ontouchmove',\n 'ontouchcancel',\n 'onpointerdown',\n 'onpointerup',\n 'onpointermove',\n 'onanimationstart',\n 'onanimationend',\n 'onanimationiteration',\n 'ontransitionstart',\n 'ontransitionend',\n 'ontransitioncancel',\n]);\n\n/** 需要 URL 安全检查的属性 */\nconst URL_ATTRS = new Set(['href', 'src', 'action', 'formaction', 'xlink:href', 'data', 'poster']);\n\n/** 安全的 URL 协议白名单 */\nconst SAFE_URL_PROTOCOLS = new Set([\n 'http:',\n 'https:',\n 'mailto:',\n 'tel:',\n 'ftp:',\n '.',\n '/',\n '#',\n '?',\n]);\n\n/**\n * 检查属性名是否安全(非事件处理器)\n */\nfunction isSafeAttr(key: string): boolean {\n return !DANGEROUS_EVENT_ATTRS.has(key.toLowerCase());\n}\n\n/**\n * 检查 URL 属性值是否安全(非 javascript: 等危险协议)\n */\nfunction isSafeURL(value: string): boolean {\n const trimmed = value.trim().toLowerCase();\n // 相对 URL、hash、query 是安全的\n if (\n trimmed.startsWith('#') ||\n trimmed.startsWith('?') ||\n trimmed.startsWith('/') ||\n trimmed.startsWith('.')\n ) {\n return true;\n }\n // 检查协议\n const colonIndex = trimmed.indexOf(':');\n if (colonIndex > 0) {\n const protocol = trimmed.slice(0, colonIndex + 1);\n return SAFE_URL_PROTOCOLS.has(protocol);\n }\n return true;\n}\n\n/**\n * 创建 DOM 元素\n *\n * @param tag - 标签名\n * @param attrs - 属性对象(可选)\n * @param children - 子元素列表,字符串会创建 TextNode(可选)\n * @returns 创建的 Element\n */\nexport function createElement(\n tag: string,\n attrs?: Record<string, string>,\n children?: (string | Node)[],\n): Element {\n if (!isBrowser) {\n throw new Error('createElement is only available in browser environments');\n }\n\n const el = document.createElement(tag);\n\n if (attrs) {\n for (const key of Object.keys(attrs)) {\n const val = attrs[key];\n if (val !== undefined && isSafeAttr(key)) {\n // 对 URL 类属性进行协议安全检查\n if (URL_ATTRS.has(key.toLowerCase()) && !isSafeURL(val)) {\n continue; // 跳过危险的 URL 属性值\n }\n el.setAttribute(key, val);\n }\n }\n }\n\n if (children) {\n for (const child of children) {\n if (typeof child === 'string') {\n el.appendChild(document.createTextNode(child));\n } else {\n el.appendChild(child);\n }\n }\n }\n\n return el;\n}\n\n/**\n * 在参考节点前插入子节点\n *\n * @param parent - 父节点\n * @param child - 要插入的子节点\n * @param ref - 参考节点,为 null 时等同于 appendChild\n */\nexport function insertBefore(parent: Node, child: Node, ref: Node | null): void {\n if (!isBrowser) {\n throw new Error('insertBefore is only available in browser environments');\n }\n\n parent.insertBefore(child, ref);\n}\n\n/**\n * 移除子节点\n *\n * @param parent - 父节点\n * @param child - 要移除的子节点\n * @returns 成功返回 true,失败返回 false\n */\nexport function removeChild(parent: Node, child: Node): boolean {\n if (!isBrowser) {\n return false;\n }\n\n try {\n parent.removeChild(child);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * 获取下一个兄弟节点\n *\n * @param node - 当前节点\n * @param skipComments - 是否跳过注释节点,默认为 false\n * @returns 下一个兄弟节点,如果没有则返回 null\n */\nexport function nextSibling(node: Node, skipComments: boolean = false): Node | null {\n if (!isBrowser) {\n return null;\n }\n\n let sibling = node.nextSibling;\n if (skipComments) {\n while (sibling && sibling.nodeType === Node.COMMENT_NODE) {\n sibling = sibling.nextSibling;\n }\n }\n return sibling;\n}\n\n/**\n * 创建文本节点\n *\n * @param text - 文本内容\n * @returns Text 节点\n */\nexport function createTextNode(text: string): Text {\n if (!isBrowser) {\n throw new Error('createTextNode is only available in browser environments');\n }\n\n return document.createTextNode(text);\n}\n\n/**\n * 创建注释节点\n *\n * @param text - 注释内容\n * @returns Comment 节点\n */\nexport function createComment(text: string): Comment {\n if (!isBrowser) {\n throw new Error('createComment is only available in browser environments');\n }\n\n return document.createComment(text);\n}\n\n/**\n * 批量设置元素样式\n *\n * @param el - 目标元素\n * @param styles - 样式键值对\n */\nexport function setStyle(el: Element, styles: Record<string, string | number>): void {\n if (!isBrowser) {\n throw new Error('setStyle is only available in browser environments');\n }\n\n const htmlEl = el as HTMLElement;\n for (const key of Object.keys(styles)) {\n (htmlEl.style as unknown as Record<string, string>)[key] = String(styles[key]);\n }\n}\n\n/**\n * 检查元素是否有指定 class\n *\n * @param el - 目标元素\n * @param cls - class 名称\n * @returns 是否包含该 class\n */\nexport function hasClass(el: Element, cls: string): boolean {\n if (!isBrowser) {\n return false;\n }\n\n return el.classList.contains(cls);\n}\n\n/**\n * 添加 class\n *\n * @param el - 目标元素\n * @param cls - 要添加的 class 名称列表\n */\nexport function addClass(el: Element, ...cls: string[]): void {\n if (!isBrowser) {\n return;\n }\n\n el.classList.add(...cls);\n}\n\n/**\n * 移除 class\n *\n * @param el - 目标元素\n * @param cls - 要移除的 class 名称列表\n */\nexport function removeClass(el: Element, ...cls: string[]): void {\n if (!isBrowser) {\n return;\n }\n\n el.classList.remove(...cls);\n}\n"]}
package/dist/index.mjs CHANGED
@@ -53,15 +53,7 @@ var DANGEROUS_EVENT_ATTRS = /* @__PURE__ */ new Set([
53
53
  "ontransitionend",
54
54
  "ontransitioncancel"
55
55
  ]);
56
- var URL_ATTRS = /* @__PURE__ */ new Set([
57
- "href",
58
- "src",
59
- "action",
60
- "formaction",
61
- "xlink:href",
62
- "data",
63
- "poster"
64
- ]);
56
+ var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "data", "poster"]);
65
57
  var SAFE_URL_PROTOCOLS = /* @__PURE__ */ new Set([
66
58
  "http:",
67
59
  "https:",
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";AAKA,IAAM,SAAA,GACJ,OAAO,MAAA,KAAW,WAAA,IAClB,OAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,CAAS,aAAA,KAAkB,UAAA;AAGpC,IAAM,qBAAA,uBAA4B,GAAA,CAAI;AAAA,EACpC,SAAA;AAAA,EAAW,YAAA;AAAA,EAAc,aAAA;AAAA,EAAe,WAAA;AAAA,EAAa,aAAA;AAAA,EACrD,aAAA;AAAA,EAAe,YAAA;AAAA,EAAc,cAAA;AAAA,EAAgB,cAAA;AAAA,EAC7C,WAAA;AAAA,EAAa,SAAA;AAAA,EAAW,YAAA;AAAA,EACxB,SAAA;AAAA,EAAW,QAAA;AAAA,EAAU,SAAA;AAAA,EAAW,UAAA;AAAA,EAAY,UAAA;AAAA,EAAY,SAAA;AAAA,EACxD,QAAA;AAAA,EAAU,UAAA;AAAA,EAAY,gBAAA;AAAA,EAAkB,SAAA;AAAA,EAAW,UAAA;AAAA,EACnD,UAAA;AAAA,EAAY,SAAA;AAAA,EAAW,QAAA;AAAA,EAAU,aAAA;AAAA,EAAe,WAAA;AAAA,EAChD,aAAA;AAAA,EAAe,aAAA;AAAA,EAAe,YAAA;AAAA,EAAc,QAAA;AAAA,EAC5C,QAAA;AAAA,EAAU,OAAA;AAAA,EAAS,SAAA;AAAA,EACnB,eAAA;AAAA,EAAiB,UAAA;AAAA,EAAY,eAAA;AAAA,EAC7B,cAAA;AAAA,EAAgB,YAAA;AAAA,EAAc,aAAA;AAAA,EAAe,eAAA;AAAA,EAC7C,eAAA;AAAA,EAAiB,aAAA;AAAA,EAAe,eAAA;AAAA,EAChC,kBAAA;AAAA,EAAoB,gBAAA;AAAA,EAAkB,sBAAA;AAAA,EACtC,mBAAA;AAAA,EAAqB,iBAAA;AAAA,EAAmB;AAC1C,CAAC,CAAA;AAGD,IAAM,SAAA,uBAAgB,GAAA,CAAI;AAAA,EACxB,MAAA;AAAA,EAAQ,KAAA;AAAA,EAAO,QAAA;AAAA,EAAU,YAAA;AAAA,EAAc,YAAA;AAAA,EAAc,MAAA;AAAA,EAAQ;AAC/D,CAAC,CAAA;AAGD,IAAM,kBAAA,uBAAyB,GAAA,CAAI;AAAA,EACjC,OAAA;AAAA,EAAS,QAAA;AAAA,EAAU,SAAA;AAAA,EAAW,MAAA;AAAA,EAAQ,MAAA;AAAA,EAAQ,GAAA;AAAA,EAAK,GAAA;AAAA,EAAK,GAAA;AAAA,EAAK;AAC/D,CAAC,CAAA;AAKD,SAAS,WAAW,GAAA,EAAsB;AACxC,EAAA,OAAO,CAAC,qBAAA,CAAsB,GAAA,CAAI,GAAA,CAAI,aAAa,CAAA;AACrD;AAKA,SAAS,UAAU,KAAA,EAAwB;AACzC,EAAA,MAAM,OAAA,GAAU,KAAA,CAAM,IAAA,EAAK,CAAE,WAAA,EAAY;AAEzC,EAAA,IAAI,OAAA,CAAQ,UAAA,CAAW,GAAG,CAAA,IAAK,QAAQ,UAAA,CAAW,GAAG,CAAA,IAAK,OAAA,CAAQ,WAAW,GAAG,CAAA,IAAK,OAAA,CAAQ,UAAA,CAAW,GAAG,CAAA,EAAG;AAC5G,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACtC,EAAA,IAAI,aAAa,CAAA,EAAG;AAClB,IAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,aAAa,CAAC,CAAA;AAChD,IAAA,OAAO,kBAAA,CAAmB,IAAI,QAAQ,CAAA;AAAA,EACxC;AACA,EAAA,OAAO,IAAA;AACT;AAUO,SAAS,aAAA,CACd,GAAA,EACA,KAAA,EACA,QAAA,EACS;AACT,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,MAAM,IAAI,MAAM,yDAAyD,CAAA;AAAA,EAC3E;AAEA,EAAA,MAAM,EAAA,GAAK,QAAA,CAAS,aAAA,CAAc,GAAG,CAAA;AAErC,EAAA,IAAI,KAAA,EAAO;AACT,IAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,EAAG;AACpC,MAAA,MAAM,GAAA,GAAM,MAAM,GAAG,CAAA;AACrB,MAAA,IAAI,GAAA,KAAQ,MAAA,IAAa,UAAA,CAAW,GAAG,CAAA,EAAG;AAExC,QAAA,IAAI,SAAA,CAAU,IAAI,GAAA,CAAI,WAAA,EAAa,CAAA,IAAK,CAAC,SAAA,CAAU,GAAG,CAAA,EAAG;AACvD,UAAA;AAAA,QACF;AACA,QAAA,EAAA,CAAG,YAAA,CAAa,KAAK,GAAG,CAAA;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AAEA,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,KAAA,MAAW,SAAS,QAAA,EAAU;AAC5B,MAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,QAAA,EAAA,CAAG,WAAA,CAAY,QAAA,CAAS,cAAA,CAAe,KAAK,CAAC,CAAA;AAAA,MAC/C,CAAA,MAAO;AACL,QAAA,EAAA,CAAG,YAAY,KAAK,CAAA;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAA;AACT;AASO,SAAS,YAAA,CACd,MAAA,EACA,KAAA,EACA,GAAA,EACM;AACN,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,MAAM,IAAI,MAAM,wDAAwD,CAAA;AAAA,EAC1E;AAEA,EAAA,MAAA,CAAO,YAAA,CAAa,OAAO,GAAG,CAAA;AAChC;AASO,SAAS,WAAA,CAAY,QAAc,KAAA,EAAsB;AAC9D,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,IAAI;AACF,IAAA,MAAA,CAAO,YAAY,KAAK,CAAA;AACxB,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AASO,SAAS,WAAA,CAAY,IAAA,EAAY,YAAA,GAAwB,KAAA,EAAoB;AAClF,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI,UAAU,IAAA,CAAK,WAAA;AACnB,EAAA,IAAI,YAAA,EAAc;AAChB,IAAA,OAAO,OAAA,IAAW,OAAA,CAAQ,QAAA,KAAa,IAAA,CAAK,YAAA,EAAc;AACxD,MAAA,OAAA,GAAU,OAAA,CAAQ,WAAA;AAAA,IACpB;AAAA,EACF;AACA,EAAA,OAAO,OAAA;AACT;AAQO,SAAS,eAAe,IAAA,EAAoB;AACjD,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,MAAM,IAAI,MAAM,0DAA0D,CAAA;AAAA,EAC5E;AAEA,EAAA,OAAO,QAAA,CAAS,eAAe,IAAI,CAAA;AACrC;AAQO,SAAS,cAAc,IAAA,EAAuB;AACnD,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,MAAM,IAAI,MAAM,yDAAyD,CAAA;AAAA,EAC3E;AAEA,EAAA,OAAO,QAAA,CAAS,cAAc,IAAI,CAAA;AACpC;AAQO,SAAS,QAAA,CACd,IACA,MAAA,EACM;AACN,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,MAAM,IAAI,MAAM,oDAAoD,CAAA;AAAA,EACtE;AAEA,EAAA,MAAM,MAAA,GAAS,EAAA;AACf,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA,EAAG;AACrC,IAAC,OAAO,KAAA,CAA4C,GAAG,IAAI,MAAA,CAAO,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,EAC/E;AACF;AASO,SAAS,QAAA,CAAS,IAAa,GAAA,EAAsB;AAC1D,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,EAAA,CAAG,SAAA,CAAU,QAAA,CAAS,GAAG,CAAA;AAClC;AAQO,SAAS,QAAA,CAAS,OAAgB,GAAA,EAAqB;AAC5D,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA;AAAA,EACF;AAEA,EAAA,EAAA,CAAG,SAAA,CAAU,GAAA,CAAI,GAAG,GAAG,CAAA;AACzB;AAQO,SAAS,WAAA,CAAY,OAAgB,GAAA,EAAqB;AAC/D,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA;AAAA,EACF;AAEA,EAAA,EAAA,CAAG,SAAA,CAAU,MAAA,CAAO,GAAG,GAAG,CAAA;AAC5B","file":"index.mjs","sourcesContent":["/**\r\n * @lytjs/common-dom-helpers\r\n * 轻量级 DOM 操作辅助工具\r\n */\r\n\r\nconst isBrowser =\r\n typeof window !== 'undefined' &&\r\n typeof document !== 'undefined' &&\r\n typeof document.createElement === 'function';\r\n\r\n/** 危险的事件属性黑名单,防止通过属性注入事件处理器 */\r\nconst DANGEROUS_EVENT_ATTRS = new Set([\r\n 'onclick', 'ondblclick', 'onmousedown', 'onmouseup', 'onmouseover',\r\n 'onmousemove', 'onmouseout', 'onmouseenter', 'onmouseleave',\r\n 'onkeydown', 'onkeyup', 'onkeypress',\r\n 'onfocus', 'onblur', 'oninput', 'onchange', 'onsubmit', 'onreset',\r\n 'onload', 'onunload', 'onbeforeunload', 'onerror', 'onresize',\r\n 'onscroll', 'onwheel', 'ondrag', 'ondragstart', 'ondragend',\r\n 'ondragenter', 'ondragleave', 'ondragover', 'ondrop',\r\n 'oncopy', 'oncut', 'onpaste',\r\n 'oncontextmenu', 'onselect', 'onselectstart',\r\n 'ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel',\r\n 'onpointerdown', 'onpointerup', 'onpointermove',\r\n 'onanimationstart', 'onanimationend', 'onanimationiteration',\r\n 'ontransitionstart', 'ontransitionend', 'ontransitioncancel',\r\n]);\r\n\r\n/** 需要 URL 安全检查的属性 */\r\nconst URL_ATTRS = new Set([\r\n 'href', 'src', 'action', 'formaction', 'xlink:href', 'data', 'poster',\r\n]);\r\n\r\n/** 安全的 URL 协议白名单 */\r\nconst SAFE_URL_PROTOCOLS = new Set([\r\n 'http:', 'https:', 'mailto:', 'tel:', 'ftp:', '.', '/', '#', '?',\r\n]);\r\n\r\n/**\r\n * 检查属性名是否安全(非事件处理器)\r\n */\r\nfunction isSafeAttr(key: string): boolean {\r\n return !DANGEROUS_EVENT_ATTRS.has(key.toLowerCase());\r\n}\r\n\r\n/**\r\n * 检查 URL 属性值是否安全(非 javascript: 等危险协议)\r\n */\r\nfunction isSafeURL(value: string): boolean {\r\n const trimmed = value.trim().toLowerCase();\r\n // 相对 URL、hash、query 是安全的\r\n if (trimmed.startsWith('#') || trimmed.startsWith('?') || trimmed.startsWith('/') || trimmed.startsWith('.')) {\r\n return true;\r\n }\r\n // 检查协议\r\n const colonIndex = trimmed.indexOf(':');\r\n if (colonIndex > 0) {\r\n const protocol = trimmed.slice(0, colonIndex + 1);\r\n return SAFE_URL_PROTOCOLS.has(protocol);\r\n }\r\n return true;\r\n}\r\n\r\n/**\r\n * 创建 DOM 元素\r\n *\r\n * @param tag - 标签名\r\n * @param attrs - 属性对象(可选)\r\n * @param children - 子元素列表,字符串会创建 TextNode(可选)\r\n * @returns 创建的 Element\r\n */\r\nexport function createElement(\r\n tag: string,\r\n attrs?: Record<string, string>,\r\n children?: (string | Node)[],\r\n): Element {\r\n if (!isBrowser) {\r\n throw new Error('createElement is only available in browser environments');\r\n }\r\n\r\n const el = document.createElement(tag);\r\n\r\n if (attrs) {\r\n for (const key of Object.keys(attrs)) {\r\n const val = attrs[key];\r\n if (val !== undefined && isSafeAttr(key)) {\r\n // 对 URL 类属性进行协议安全检查\r\n if (URL_ATTRS.has(key.toLowerCase()) && !isSafeURL(val)) {\r\n continue; // 跳过危险的 URL 属性值\r\n }\r\n el.setAttribute(key, val);\r\n }\r\n }\r\n }\r\n\r\n if (children) {\r\n for (const child of children) {\r\n if (typeof child === 'string') {\r\n el.appendChild(document.createTextNode(child));\r\n } else {\r\n el.appendChild(child);\r\n }\r\n }\r\n }\r\n\r\n return el;\r\n}\r\n\r\n/**\r\n * 在参考节点前插入子节点\r\n *\r\n * @param parent - 父节点\r\n * @param child - 要插入的子节点\r\n * @param ref - 参考节点,为 null 时等同于 appendChild\r\n */\r\nexport function insertBefore(\r\n parent: Node,\r\n child: Node,\r\n ref: Node | null,\r\n): void {\r\n if (!isBrowser) {\r\n throw new Error('insertBefore is only available in browser environments');\r\n }\r\n\r\n parent.insertBefore(child, ref);\r\n}\r\n\r\n/**\r\n * 移除子节点\r\n *\r\n * @param parent - 父节点\r\n * @param child - 要移除的子节点\r\n * @returns 成功返回 true,失败返回 false\r\n */\r\nexport function removeChild(parent: Node, child: Node): boolean {\r\n if (!isBrowser) {\r\n return false;\r\n }\r\n\r\n try {\r\n parent.removeChild(child);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * 获取下一个兄弟节点\r\n *\r\n * @param node - 当前节点\r\n * @param skipComments - 是否跳过注释节点,默认为 false\r\n * @returns 下一个兄弟节点,如果没有则返回 null\r\n */\r\nexport function nextSibling(node: Node, skipComments: boolean = false): Node | null {\r\n if (!isBrowser) {\r\n return null;\r\n }\r\n\r\n let sibling = node.nextSibling;\r\n if (skipComments) {\r\n while (sibling && sibling.nodeType === Node.COMMENT_NODE) {\r\n sibling = sibling.nextSibling;\r\n }\r\n }\r\n return sibling;\r\n}\r\n\r\n/**\r\n * 创建文本节点\r\n *\r\n * @param text - 文本内容\r\n * @returns Text 节点\r\n */\r\nexport function createTextNode(text: string): Text {\r\n if (!isBrowser) {\r\n throw new Error('createTextNode is only available in browser environments');\r\n }\r\n\r\n return document.createTextNode(text);\r\n}\r\n\r\n/**\r\n * 创建注释节点\r\n *\r\n * @param text - 注释内容\r\n * @returns Comment 节点\r\n */\r\nexport function createComment(text: string): Comment {\r\n if (!isBrowser) {\r\n throw new Error('createComment is only available in browser environments');\r\n }\r\n\r\n return document.createComment(text);\r\n}\r\n\r\n/**\r\n * 批量设置元素样式\r\n *\r\n * @param el - 目标元素\r\n * @param styles - 样式键值对\r\n */\r\nexport function setStyle(\r\n el: Element,\r\n styles: Record<string, string | number>,\r\n): void {\r\n if (!isBrowser) {\r\n throw new Error('setStyle is only available in browser environments');\r\n }\r\n\r\n const htmlEl = el as HTMLElement;\r\n for (const key of Object.keys(styles)) {\r\n (htmlEl.style as unknown as Record<string, string>)[key] = String(styles[key]);\r\n }\r\n}\r\n\r\n/**\r\n * 检查元素是否有指定 class\r\n *\r\n * @param el - 目标元素\r\n * @param cls - class 名称\r\n * @returns 是否包含该 class\r\n */\r\nexport function hasClass(el: Element, cls: string): boolean {\r\n if (!isBrowser) {\r\n return false;\r\n }\r\n\r\n return el.classList.contains(cls);\r\n}\r\n\r\n/**\r\n * 添加 class\r\n *\r\n * @param el - 目标元素\r\n * @param cls - 要添加的 class 名称列表\r\n */\r\nexport function addClass(el: Element, ...cls: string[]): void {\r\n if (!isBrowser) {\r\n return;\r\n }\r\n\r\n el.classList.add(...cls);\r\n}\r\n\r\n/**\r\n * 移除 class\r\n *\r\n * @param el - 目标元素\r\n * @param cls - 要移除的 class 名称列表\r\n */\r\nexport function removeClass(el: Element, ...cls: string[]): void {\r\n if (!isBrowser) {\r\n return;\r\n }\r\n\r\n el.classList.remove(...cls);\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @lytjs/common-dom-helpers\n * 轻量级 DOM 操作辅助工具\n */\n\nconst isBrowser =\n typeof window !== 'undefined' &&\n typeof document !== 'undefined' &&\n typeof document.createElement === 'function';\n\n/** 危险的事件属性黑名单,防止通过属性注入事件处理器 */\nconst DANGEROUS_EVENT_ATTRS = new Set([\n 'onclick',\n 'ondblclick',\n 'onmousedown',\n 'onmouseup',\n 'onmouseover',\n 'onmousemove',\n 'onmouseout',\n 'onmouseenter',\n 'onmouseleave',\n 'onkeydown',\n 'onkeyup',\n 'onkeypress',\n 'onfocus',\n 'onblur',\n 'oninput',\n 'onchange',\n 'onsubmit',\n 'onreset',\n 'onload',\n 'onunload',\n 'onbeforeunload',\n 'onerror',\n 'onresize',\n 'onscroll',\n 'onwheel',\n 'ondrag',\n 'ondragstart',\n 'ondragend',\n 'ondragenter',\n 'ondragleave',\n 'ondragover',\n 'ondrop',\n 'oncopy',\n 'oncut',\n 'onpaste',\n 'oncontextmenu',\n 'onselect',\n 'onselectstart',\n 'ontouchstart',\n 'ontouchend',\n 'ontouchmove',\n 'ontouchcancel',\n 'onpointerdown',\n 'onpointerup',\n 'onpointermove',\n 'onanimationstart',\n 'onanimationend',\n 'onanimationiteration',\n 'ontransitionstart',\n 'ontransitionend',\n 'ontransitioncancel',\n]);\n\n/** 需要 URL 安全检查的属性 */\nconst URL_ATTRS = new Set(['href', 'src', 'action', 'formaction', 'xlink:href', 'data', 'poster']);\n\n/** 安全的 URL 协议白名单 */\nconst SAFE_URL_PROTOCOLS = new Set([\n 'http:',\n 'https:',\n 'mailto:',\n 'tel:',\n 'ftp:',\n '.',\n '/',\n '#',\n '?',\n]);\n\n/**\n * 检查属性名是否安全(非事件处理器)\n */\nfunction isSafeAttr(key: string): boolean {\n return !DANGEROUS_EVENT_ATTRS.has(key.toLowerCase());\n}\n\n/**\n * 检查 URL 属性值是否安全(非 javascript: 等危险协议)\n */\nfunction isSafeURL(value: string): boolean {\n const trimmed = value.trim().toLowerCase();\n // 相对 URL、hash、query 是安全的\n if (\n trimmed.startsWith('#') ||\n trimmed.startsWith('?') ||\n trimmed.startsWith('/') ||\n trimmed.startsWith('.')\n ) {\n return true;\n }\n // 检查协议\n const colonIndex = trimmed.indexOf(':');\n if (colonIndex > 0) {\n const protocol = trimmed.slice(0, colonIndex + 1);\n return SAFE_URL_PROTOCOLS.has(protocol);\n }\n return true;\n}\n\n/**\n * 创建 DOM 元素\n *\n * @param tag - 标签名\n * @param attrs - 属性对象(可选)\n * @param children - 子元素列表,字符串会创建 TextNode(可选)\n * @returns 创建的 Element\n */\nexport function createElement(\n tag: string,\n attrs?: Record<string, string>,\n children?: (string | Node)[],\n): Element {\n if (!isBrowser) {\n throw new Error('createElement is only available in browser environments');\n }\n\n const el = document.createElement(tag);\n\n if (attrs) {\n for (const key of Object.keys(attrs)) {\n const val = attrs[key];\n if (val !== undefined && isSafeAttr(key)) {\n // 对 URL 类属性进行协议安全检查\n if (URL_ATTRS.has(key.toLowerCase()) && !isSafeURL(val)) {\n continue; // 跳过危险的 URL 属性值\n }\n el.setAttribute(key, val);\n }\n }\n }\n\n if (children) {\n for (const child of children) {\n if (typeof child === 'string') {\n el.appendChild(document.createTextNode(child));\n } else {\n el.appendChild(child);\n }\n }\n }\n\n return el;\n}\n\n/**\n * 在参考节点前插入子节点\n *\n * @param parent - 父节点\n * @param child - 要插入的子节点\n * @param ref - 参考节点,为 null 时等同于 appendChild\n */\nexport function insertBefore(parent: Node, child: Node, ref: Node | null): void {\n if (!isBrowser) {\n throw new Error('insertBefore is only available in browser environments');\n }\n\n parent.insertBefore(child, ref);\n}\n\n/**\n * 移除子节点\n *\n * @param parent - 父节点\n * @param child - 要移除的子节点\n * @returns 成功返回 true,失败返回 false\n */\nexport function removeChild(parent: Node, child: Node): boolean {\n if (!isBrowser) {\n return false;\n }\n\n try {\n parent.removeChild(child);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * 获取下一个兄弟节点\n *\n * @param node - 当前节点\n * @param skipComments - 是否跳过注释节点,默认为 false\n * @returns 下一个兄弟节点,如果没有则返回 null\n */\nexport function nextSibling(node: Node, skipComments: boolean = false): Node | null {\n if (!isBrowser) {\n return null;\n }\n\n let sibling = node.nextSibling;\n if (skipComments) {\n while (sibling && sibling.nodeType === Node.COMMENT_NODE) {\n sibling = sibling.nextSibling;\n }\n }\n return sibling;\n}\n\n/**\n * 创建文本节点\n *\n * @param text - 文本内容\n * @returns Text 节点\n */\nexport function createTextNode(text: string): Text {\n if (!isBrowser) {\n throw new Error('createTextNode is only available in browser environments');\n }\n\n return document.createTextNode(text);\n}\n\n/**\n * 创建注释节点\n *\n * @param text - 注释内容\n * @returns Comment 节点\n */\nexport function createComment(text: string): Comment {\n if (!isBrowser) {\n throw new Error('createComment is only available in browser environments');\n }\n\n return document.createComment(text);\n}\n\n/**\n * 批量设置元素样式\n *\n * @param el - 目标元素\n * @param styles - 样式键值对\n */\nexport function setStyle(el: Element, styles: Record<string, string | number>): void {\n if (!isBrowser) {\n throw new Error('setStyle is only available in browser environments');\n }\n\n const htmlEl = el as HTMLElement;\n for (const key of Object.keys(styles)) {\n (htmlEl.style as unknown as Record<string, string>)[key] = String(styles[key]);\n }\n}\n\n/**\n * 检查元素是否有指定 class\n *\n * @param el - 目标元素\n * @param cls - class 名称\n * @returns 是否包含该 class\n */\nexport function hasClass(el: Element, cls: string): boolean {\n if (!isBrowser) {\n return false;\n }\n\n return el.classList.contains(cls);\n}\n\n/**\n * 添加 class\n *\n * @param el - 目标元素\n * @param cls - 要添加的 class 名称列表\n */\nexport function addClass(el: Element, ...cls: string[]): void {\n if (!isBrowser) {\n return;\n }\n\n el.classList.add(...cls);\n}\n\n/**\n * 移除 class\n *\n * @param el - 目标元素\n * @param cls - 要移除的 class 名称列表\n */\nexport function removeClass(el: Element, ...cls: string[]): void {\n if (!isBrowser) {\n return;\n }\n\n el.classList.remove(...cls);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lytjs/common-dom-helpers",
3
- "version": "6.5.0",
3
+ "version": "6.7.0",
4
4
  "description": "Lightweight DOM manipulation helpers for LytJS",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
package/src/env.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // 全局 __DEV__ 声明
2
- // 规范版本位于 @lytjs/shared-types/src/global.d.ts
3
- // 此处保留直接声明以确保 tsup DTS 构建时类型可用
4
- declare const __DEV__: boolean;
1
+ // 全局 __DEV__ 声明
2
+ // 规范版本位于 @lytjs/shared-types/src/global.d.ts
3
+ // 此处保留直接声明以确保 tsup DTS 构建时类型可用
4
+ declare const __DEV__: boolean;
package/src/index.ts CHANGED
@@ -1,257 +1,299 @@
1
- /**
2
- * @lytjs/common-dom-helpers
3
- * 轻量级 DOM 操作辅助工具
4
- */
5
-
6
- const isBrowser =
7
- typeof window !== 'undefined' &&
8
- typeof document !== 'undefined' &&
9
- typeof document.createElement === 'function';
10
-
11
- /** 危险的事件属性黑名单,防止通过属性注入事件处理器 */
12
- const DANGEROUS_EVENT_ATTRS = new Set([
13
- 'onclick', 'ondblclick', 'onmousedown', 'onmouseup', 'onmouseover',
14
- 'onmousemove', 'onmouseout', 'onmouseenter', 'onmouseleave',
15
- 'onkeydown', 'onkeyup', 'onkeypress',
16
- 'onfocus', 'onblur', 'oninput', 'onchange', 'onsubmit', 'onreset',
17
- 'onload', 'onunload', 'onbeforeunload', 'onerror', 'onresize',
18
- 'onscroll', 'onwheel', 'ondrag', 'ondragstart', 'ondragend',
19
- 'ondragenter', 'ondragleave', 'ondragover', 'ondrop',
20
- 'oncopy', 'oncut', 'onpaste',
21
- 'oncontextmenu', 'onselect', 'onselectstart',
22
- 'ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel',
23
- 'onpointerdown', 'onpointerup', 'onpointermove',
24
- 'onanimationstart', 'onanimationend', 'onanimationiteration',
25
- 'ontransitionstart', 'ontransitionend', 'ontransitioncancel',
26
- ]);
27
-
28
- /** 需要 URL 安全检查的属性 */
29
- const URL_ATTRS = new Set([
30
- 'href', 'src', 'action', 'formaction', 'xlink:href', 'data', 'poster',
31
- ]);
32
-
33
- /** 安全的 URL 协议白名单 */
34
- const SAFE_URL_PROTOCOLS = new Set([
35
- 'http:', 'https:', 'mailto:', 'tel:', 'ftp:', '.', '/', '#', '?',
36
- ]);
37
-
38
- /**
39
- * 检查属性名是否安全(非事件处理器)
40
- */
41
- function isSafeAttr(key: string): boolean {
42
- return !DANGEROUS_EVENT_ATTRS.has(key.toLowerCase());
43
- }
44
-
45
- /**
46
- * 检查 URL 属性值是否安全(非 javascript: 等危险协议)
47
- */
48
- function isSafeURL(value: string): boolean {
49
- const trimmed = value.trim().toLowerCase();
50
- // 相对 URL、hash、query 是安全的
51
- if (trimmed.startsWith('#') || trimmed.startsWith('?') || trimmed.startsWith('/') || trimmed.startsWith('.')) {
52
- return true;
53
- }
54
- // 检查协议
55
- const colonIndex = trimmed.indexOf(':');
56
- if (colonIndex > 0) {
57
- const protocol = trimmed.slice(0, colonIndex + 1);
58
- return SAFE_URL_PROTOCOLS.has(protocol);
59
- }
60
- return true;
61
- }
62
-
63
- /**
64
- * 创建 DOM 元素
65
- *
66
- * @param tag - 标签名
67
- * @param attrs - 属性对象(可选)
68
- * @param children - 子元素列表,字符串会创建 TextNode(可选)
69
- * @returns 创建的 Element
70
- */
71
- export function createElement(
72
- tag: string,
73
- attrs?: Record<string, string>,
74
- children?: (string | Node)[],
75
- ): Element {
76
- if (!isBrowser) {
77
- throw new Error('createElement is only available in browser environments');
78
- }
79
-
80
- const el = document.createElement(tag);
81
-
82
- if (attrs) {
83
- for (const key of Object.keys(attrs)) {
84
- const val = attrs[key];
85
- if (val !== undefined && isSafeAttr(key)) {
86
- // 对 URL 类属性进行协议安全检查
87
- if (URL_ATTRS.has(key.toLowerCase()) && !isSafeURL(val)) {
88
- continue; // 跳过危险的 URL 属性值
89
- }
90
- el.setAttribute(key, val);
91
- }
92
- }
93
- }
94
-
95
- if (children) {
96
- for (const child of children) {
97
- if (typeof child === 'string') {
98
- el.appendChild(document.createTextNode(child));
99
- } else {
100
- el.appendChild(child);
101
- }
102
- }
103
- }
104
-
105
- return el;
106
- }
107
-
108
- /**
109
- * 在参考节点前插入子节点
110
- *
111
- * @param parent - 父节点
112
- * @param child - 要插入的子节点
113
- * @param ref - 参考节点,为 null 时等同于 appendChild
114
- */
115
- export function insertBefore(
116
- parent: Node,
117
- child: Node,
118
- ref: Node | null,
119
- ): void {
120
- if (!isBrowser) {
121
- throw new Error('insertBefore is only available in browser environments');
122
- }
123
-
124
- parent.insertBefore(child, ref);
125
- }
126
-
127
- /**
128
- * 移除子节点
129
- *
130
- * @param parent - 父节点
131
- * @param child - 要移除的子节点
132
- * @returns 成功返回 true,失败返回 false
133
- */
134
- export function removeChild(parent: Node, child: Node): boolean {
135
- if (!isBrowser) {
136
- return false;
137
- }
138
-
139
- try {
140
- parent.removeChild(child);
141
- return true;
142
- } catch {
143
- return false;
144
- }
145
- }
146
-
147
- /**
148
- * 获取下一个兄弟节点
149
- *
150
- * @param node - 当前节点
151
- * @param skipComments - 是否跳过注释节点,默认为 false
152
- * @returns 下一个兄弟节点,如果没有则返回 null
153
- */
154
- export function nextSibling(node: Node, skipComments: boolean = false): Node | null {
155
- if (!isBrowser) {
156
- return null;
157
- }
158
-
159
- let sibling = node.nextSibling;
160
- if (skipComments) {
161
- while (sibling && sibling.nodeType === Node.COMMENT_NODE) {
162
- sibling = sibling.nextSibling;
163
- }
164
- }
165
- return sibling;
166
- }
167
-
168
- /**
169
- * 创建文本节点
170
- *
171
- * @param text - 文本内容
172
- * @returns Text 节点
173
- */
174
- export function createTextNode(text: string): Text {
175
- if (!isBrowser) {
176
- throw new Error('createTextNode is only available in browser environments');
177
- }
178
-
179
- return document.createTextNode(text);
180
- }
181
-
182
- /**
183
- * 创建注释节点
184
- *
185
- * @param text - 注释内容
186
- * @returns Comment 节点
187
- */
188
- export function createComment(text: string): Comment {
189
- if (!isBrowser) {
190
- throw new Error('createComment is only available in browser environments');
191
- }
192
-
193
- return document.createComment(text);
194
- }
195
-
196
- /**
197
- * 批量设置元素样式
198
- *
199
- * @param el - 目标元素
200
- * @param styles - 样式键值对
201
- */
202
- export function setStyle(
203
- el: Element,
204
- styles: Record<string, string | number>,
205
- ): void {
206
- if (!isBrowser) {
207
- throw new Error('setStyle is only available in browser environments');
208
- }
209
-
210
- const htmlEl = el as HTMLElement;
211
- for (const key of Object.keys(styles)) {
212
- (htmlEl.style as unknown as Record<string, string>)[key] = String(styles[key]);
213
- }
214
- }
215
-
216
- /**
217
- * 检查元素是否有指定 class
218
- *
219
- * @param el - 目标元素
220
- * @param cls - class 名称
221
- * @returns 是否包含该 class
222
- */
223
- export function hasClass(el: Element, cls: string): boolean {
224
- if (!isBrowser) {
225
- return false;
226
- }
227
-
228
- return el.classList.contains(cls);
229
- }
230
-
231
- /**
232
- * 添加 class
233
- *
234
- * @param el - 目标元素
235
- * @param cls - 要添加的 class 名称列表
236
- */
237
- export function addClass(el: Element, ...cls: string[]): void {
238
- if (!isBrowser) {
239
- return;
240
- }
241
-
242
- el.classList.add(...cls);
243
- }
244
-
245
- /**
246
- * 移除 class
247
- *
248
- * @param el - 目标元素
249
- * @param cls - 要移除的 class 名称列表
250
- */
251
- export function removeClass(el: Element, ...cls: string[]): void {
252
- if (!isBrowser) {
253
- return;
254
- }
255
-
256
- el.classList.remove(...cls);
257
- }
1
+ /**
2
+ * @lytjs/common-dom-helpers
3
+ * 轻量级 DOM 操作辅助工具
4
+ */
5
+
6
+ const isBrowser =
7
+ typeof window !== 'undefined' &&
8
+ typeof document !== 'undefined' &&
9
+ typeof document.createElement === 'function';
10
+
11
+ /** 危险的事件属性黑名单,防止通过属性注入事件处理器 */
12
+ const DANGEROUS_EVENT_ATTRS = new Set([
13
+ 'onclick',
14
+ 'ondblclick',
15
+ 'onmousedown',
16
+ 'onmouseup',
17
+ 'onmouseover',
18
+ 'onmousemove',
19
+ 'onmouseout',
20
+ 'onmouseenter',
21
+ 'onmouseleave',
22
+ 'onkeydown',
23
+ 'onkeyup',
24
+ 'onkeypress',
25
+ 'onfocus',
26
+ 'onblur',
27
+ 'oninput',
28
+ 'onchange',
29
+ 'onsubmit',
30
+ 'onreset',
31
+ 'onload',
32
+ 'onunload',
33
+ 'onbeforeunload',
34
+ 'onerror',
35
+ 'onresize',
36
+ 'onscroll',
37
+ 'onwheel',
38
+ 'ondrag',
39
+ 'ondragstart',
40
+ 'ondragend',
41
+ 'ondragenter',
42
+ 'ondragleave',
43
+ 'ondragover',
44
+ 'ondrop',
45
+ 'oncopy',
46
+ 'oncut',
47
+ 'onpaste',
48
+ 'oncontextmenu',
49
+ 'onselect',
50
+ 'onselectstart',
51
+ 'ontouchstart',
52
+ 'ontouchend',
53
+ 'ontouchmove',
54
+ 'ontouchcancel',
55
+ 'onpointerdown',
56
+ 'onpointerup',
57
+ 'onpointermove',
58
+ 'onanimationstart',
59
+ 'onanimationend',
60
+ 'onanimationiteration',
61
+ 'ontransitionstart',
62
+ 'ontransitionend',
63
+ 'ontransitioncancel',
64
+ ]);
65
+
66
+ /** 需要 URL 安全检查的属性 */
67
+ const URL_ATTRS = new Set(['href', 'src', 'action', 'formaction', 'xlink:href', 'data', 'poster']);
68
+
69
+ /** 安全的 URL 协议白名单 */
70
+ const SAFE_URL_PROTOCOLS = new Set([
71
+ 'http:',
72
+ 'https:',
73
+ 'mailto:',
74
+ 'tel:',
75
+ 'ftp:',
76
+ '.',
77
+ '/',
78
+ '#',
79
+ '?',
80
+ ]);
81
+
82
+ /**
83
+ * 检查属性名是否安全(非事件处理器)
84
+ */
85
+ function isSafeAttr(key: string): boolean {
86
+ return !DANGEROUS_EVENT_ATTRS.has(key.toLowerCase());
87
+ }
88
+
89
+ /**
90
+ * 检查 URL 属性值是否安全(非 javascript: 等危险协议)
91
+ */
92
+ function isSafeURL(value: string): boolean {
93
+ const trimmed = value.trim().toLowerCase();
94
+ // 相对 URL、hash、query 是安全的
95
+ if (
96
+ trimmed.startsWith('#') ||
97
+ trimmed.startsWith('?') ||
98
+ trimmed.startsWith('/') ||
99
+ trimmed.startsWith('.')
100
+ ) {
101
+ return true;
102
+ }
103
+ // 检查协议
104
+ const colonIndex = trimmed.indexOf(':');
105
+ if (colonIndex > 0) {
106
+ const protocol = trimmed.slice(0, colonIndex + 1);
107
+ return SAFE_URL_PROTOCOLS.has(protocol);
108
+ }
109
+ return true;
110
+ }
111
+
112
+ /**
113
+ * 创建 DOM 元素
114
+ *
115
+ * @param tag - 标签名
116
+ * @param attrs - 属性对象(可选)
117
+ * @param children - 子元素列表,字符串会创建 TextNode(可选)
118
+ * @returns 创建的 Element
119
+ */
120
+ export function createElement(
121
+ tag: string,
122
+ attrs?: Record<string, string>,
123
+ children?: (string | Node)[],
124
+ ): Element {
125
+ if (!isBrowser) {
126
+ throw new Error('createElement is only available in browser environments');
127
+ }
128
+
129
+ const el = document.createElement(tag);
130
+
131
+ if (attrs) {
132
+ for (const key of Object.keys(attrs)) {
133
+ const val = attrs[key];
134
+ if (val !== undefined && isSafeAttr(key)) {
135
+ // URL 类属性进行协议安全检查
136
+ if (URL_ATTRS.has(key.toLowerCase()) && !isSafeURL(val)) {
137
+ continue; // 跳过危险的 URL 属性值
138
+ }
139
+ el.setAttribute(key, val);
140
+ }
141
+ }
142
+ }
143
+
144
+ if (children) {
145
+ for (const child of children) {
146
+ if (typeof child === 'string') {
147
+ el.appendChild(document.createTextNode(child));
148
+ } else {
149
+ el.appendChild(child);
150
+ }
151
+ }
152
+ }
153
+
154
+ return el;
155
+ }
156
+
157
+ /**
158
+ * 在参考节点前插入子节点
159
+ *
160
+ * @param parent - 父节点
161
+ * @param child - 要插入的子节点
162
+ * @param ref - 参考节点,为 null 时等同于 appendChild
163
+ */
164
+ export function insertBefore(parent: Node, child: Node, ref: Node | null): void {
165
+ if (!isBrowser) {
166
+ throw new Error('insertBefore is only available in browser environments');
167
+ }
168
+
169
+ parent.insertBefore(child, ref);
170
+ }
171
+
172
+ /**
173
+ * 移除子节点
174
+ *
175
+ * @param parent - 父节点
176
+ * @param child - 要移除的子节点
177
+ * @returns 成功返回 true,失败返回 false
178
+ */
179
+ export function removeChild(parent: Node, child: Node): boolean {
180
+ if (!isBrowser) {
181
+ return false;
182
+ }
183
+
184
+ try {
185
+ parent.removeChild(child);
186
+ return true;
187
+ } catch {
188
+ return false;
189
+ }
190
+ }
191
+
192
+ /**
193
+ * 获取下一个兄弟节点
194
+ *
195
+ * @param node - 当前节点
196
+ * @param skipComments - 是否跳过注释节点,默认为 false
197
+ * @returns 下一个兄弟节点,如果没有则返回 null
198
+ */
199
+ export function nextSibling(node: Node, skipComments: boolean = false): Node | null {
200
+ if (!isBrowser) {
201
+ return null;
202
+ }
203
+
204
+ let sibling = node.nextSibling;
205
+ if (skipComments) {
206
+ while (sibling && sibling.nodeType === Node.COMMENT_NODE) {
207
+ sibling = sibling.nextSibling;
208
+ }
209
+ }
210
+ return sibling;
211
+ }
212
+
213
+ /**
214
+ * 创建文本节点
215
+ *
216
+ * @param text - 文本内容
217
+ * @returns Text 节点
218
+ */
219
+ export function createTextNode(text: string): Text {
220
+ if (!isBrowser) {
221
+ throw new Error('createTextNode is only available in browser environments');
222
+ }
223
+
224
+ return document.createTextNode(text);
225
+ }
226
+
227
+ /**
228
+ * 创建注释节点
229
+ *
230
+ * @param text - 注释内容
231
+ * @returns Comment 节点
232
+ */
233
+ export function createComment(text: string): Comment {
234
+ if (!isBrowser) {
235
+ throw new Error('createComment is only available in browser environments');
236
+ }
237
+
238
+ return document.createComment(text);
239
+ }
240
+
241
+ /**
242
+ * 批量设置元素样式
243
+ *
244
+ * @param el - 目标元素
245
+ * @param styles - 样式键值对
246
+ */
247
+ export function setStyle(el: Element, styles: Record<string, string | number>): void {
248
+ if (!isBrowser) {
249
+ throw new Error('setStyle is only available in browser environments');
250
+ }
251
+
252
+ const htmlEl = el as HTMLElement;
253
+ for (const key of Object.keys(styles)) {
254
+ (htmlEl.style as unknown as Record<string, string>)[key] = String(styles[key]);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * 检查元素是否有指定 class
260
+ *
261
+ * @param el - 目标元素
262
+ * @param cls - class 名称
263
+ * @returns 是否包含该 class
264
+ */
265
+ export function hasClass(el: Element, cls: string): boolean {
266
+ if (!isBrowser) {
267
+ return false;
268
+ }
269
+
270
+ return el.classList.contains(cls);
271
+ }
272
+
273
+ /**
274
+ * 添加 class
275
+ *
276
+ * @param el - 目标元素
277
+ * @param cls - 要添加的 class 名称列表
278
+ */
279
+ export function addClass(el: Element, ...cls: string[]): void {
280
+ if (!isBrowser) {
281
+ return;
282
+ }
283
+
284
+ el.classList.add(...cls);
285
+ }
286
+
287
+ /**
288
+ * 移除 class
289
+ *
290
+ * @param el - 目标元素
291
+ * @param cls - 要移除的 class 名称列表
292
+ */
293
+ export function removeClass(el: Element, ...cls: string[]): void {
294
+ if (!isBrowser) {
295
+ return;
296
+ }
297
+
298
+ el.classList.remove(...cls);
299
+ }