@vobs/layout 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.
package/src/utils.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { effect } from '@vobs/reactivity'
2
+ import {
3
+ createFragment,
4
+ createText,
5
+ insertBefore,
6
+ insertDynamic,
7
+ setAttribute,
8
+ setProperty,
9
+ setTextContent,
10
+ type VobsNode
11
+ } from '@vobs/vobs'
12
+ import type { LayoutAttributeValue, LayoutCommonProps } from './types'
13
+
14
+ export function readProp<T>(props: object, name: string, fallback: T): T {
15
+ const value = Reflect.get(props, name)
16
+ return (value === undefined ? fallback : value) as T
17
+ }
18
+
19
+ export function hasProp(props: object, name: string): boolean {
20
+ return name in props
21
+ }
22
+
23
+ export function classNames(...values: readonly (string | false | null | undefined)[]): string {
24
+ return values
25
+ .flatMap(value => typeof value === 'string' ? value.trim().split(/\s+/u) : [])
26
+ .filter(Boolean)
27
+ .join(' ')
28
+ }
29
+
30
+ export function bindClassList(
31
+ root: Element,
32
+ props: object,
33
+ readBaseClasses: () => readonly (string | undefined)[]
34
+ ): void {
35
+ effect(() => {
36
+ setAttribute(root, 'class', classNames(
37
+ ...readBaseClasses(),
38
+ readString(props, 'class'),
39
+ readString(props, 'className')
40
+ ))
41
+ })
42
+ }
43
+
44
+ export function bindCommonAttributes(
45
+ root: Element,
46
+ props: object,
47
+ skip: readonly string[] = []
48
+ ): void {
49
+ const ignored = new Set(skip)
50
+ ignored.add('class')
51
+ ignored.add('className')
52
+ ignored.add('children')
53
+ ignored.add('style')
54
+
55
+ effect(() => {
56
+ const next = new Map<string, string>()
57
+ for (const name of Object.keys(props)) {
58
+ if (ignored.has(name) || !isCommonAttribute(name)) continue
59
+ const normalized = normalizeAttribute(name, Reflect.get(props, name))
60
+ if (normalized) next.set(normalized.name, normalized.value)
61
+ }
62
+
63
+ const managed = managedAttributes.get(root) ?? new Set<string>()
64
+ for (const name of managed) {
65
+ if (!next.has(name)) removeAttribute(root, name)
66
+ }
67
+ for (const [name, value] of next) setAttribute(root, name, value)
68
+ managedAttributes.set(root, new Set(next.keys()))
69
+ })
70
+ }
71
+
72
+ export function bindUserStyle(root: Element, props: object, internal?: () => string): void {
73
+ effect(() => {
74
+ const values = [internal?.() ?? '', readString(props, 'style') ?? ''].filter(Boolean)
75
+ if (values.length > 0) setAttribute(root, 'style', values.join('; '))
76
+ else removeAttribute(root, 'style')
77
+ })
78
+ }
79
+
80
+ export function bindTextContent(node: Text, read: () => unknown): void {
81
+ effect(() => {
82
+ setTextContent(node, String(read() ?? ''))
83
+ })
84
+ }
85
+
86
+ export function setOptionalAttribute(node: Element, name: string, value: unknown): void {
87
+ if (value === undefined || value === null || value === false || value === '') {
88
+ removeAttribute(node, name)
89
+ return
90
+ }
91
+ setAttribute(node, name, String(value))
92
+ }
93
+
94
+ export function setOptionalProperty(node: Element, name: string, value: unknown): void {
95
+ if (value === undefined || value === null) return
96
+ setProperty(node, name, value)
97
+ }
98
+
99
+ export function mountSlot(parent: Node, props: object, name: string): void {
100
+ insertDynamic(parent, null, () => resolveSlot(Reflect.get(props, name)))
101
+ }
102
+
103
+ export function resolveSlot(value: unknown): VobsNode | null {
104
+ const resolved = typeof value === 'function' ? value() : value
105
+ if (resolved === undefined || resolved === null || resolved === false) return null
106
+ if (typeof resolved === 'string' || typeof resolved === 'number') return createText(String(resolved))
107
+ if (Array.isArray(resolved)) {
108
+ return createFragment((parent, anchor) => {
109
+ for (const child of resolved) {
110
+ const node = resolveSlot(child)
111
+ if (node) insertBefore(parent, node, anchor)
112
+ }
113
+ })
114
+ }
115
+ return resolved as VobsNode
116
+ }
117
+
118
+ function readString(props: object, name: string): string | undefined {
119
+ const value = Reflect.get(props, name)
120
+ return typeof value === 'string' ? value : undefined
121
+ }
122
+
123
+ function isCommonAttribute(name: string): boolean {
124
+ return name === 'id'
125
+ || name === 'title'
126
+ || name === 'role'
127
+ || name === 'tabIndex'
128
+ || name.startsWith('aria-')
129
+ || name.startsWith('data-')
130
+ }
131
+
132
+ function normalizeAttribute(name: string, value: unknown): { name: string; value: string } | undefined {
133
+ if (value === undefined || value === null) return undefined
134
+ if (value === false && !name.startsWith('aria-') && !name.startsWith('data-')) return undefined
135
+ return {
136
+ name: name === 'tabIndex' ? 'tabindex' : name,
137
+ value: String(value)
138
+ }
139
+ }
140
+
141
+ function removeAttribute(node: Element, name: string): void {
142
+ const candidate = node as Element & { removeAttribute?: (attribute: string) => void }
143
+ candidate.removeAttribute?.(name)
144
+ }
145
+
146
+ const managedAttributes = new WeakMap<object, Set<string>>()
147
+
148
+ export type LayoutProps = LayoutCommonProps
149
+ export type { LayoutAttributeValue }
@@ -0,0 +1,54 @@
1
+ import { getCurrentOwner, memo, onDispose, state } from '@vobs/reactivity'
2
+ import type { KitViewport } from './types'
3
+
4
+ export const DEFAULT_MOBILE_BREAKPOINT = 768
5
+
6
+ export function createKitViewport(breakpoint = DEFAULT_MOBILE_BREAKPOINT): KitViewport {
7
+ const normalizedBreakpoint = normalizeBreakpoint(breakpoint)
8
+ const width = state(readWidth(), 'layout.viewport.width')
9
+ const height = state(readHeight(), 'layout.viewport.height')
10
+ const isMobile = memo(() => width.value < normalizedBreakpoint)
11
+ let disposed = false
12
+
13
+ const onResize = (): void => {
14
+ if (disposed) return
15
+ width.value = readWidth()
16
+ height.value = readHeight()
17
+ }
18
+
19
+ if (typeof window !== 'undefined') {
20
+ window.addEventListener('resize', onResize)
21
+ }
22
+
23
+ const viewport: KitViewport = {
24
+ width,
25
+ height,
26
+ isMobile,
27
+ dispose(): void {
28
+ if (disposed) return
29
+ disposed = true
30
+ if (typeof window !== 'undefined') window.removeEventListener('resize', onResize)
31
+ isMobile.dispose()
32
+ width.dispose()
33
+ height.dispose()
34
+ }
35
+ }
36
+
37
+ if (getCurrentOwner()) onDispose(viewport.dispose)
38
+ return viewport
39
+ }
40
+
41
+ export function normalizeBreakpoint(value: number): number {
42
+ if (!Number.isFinite(value) || value <= 0) {
43
+ throw new Error('VOBS_KIT002: mobileBreakpoint 必须是大于 0 的有限数字')
44
+ }
45
+ return Math.round(value)
46
+ }
47
+
48
+ function readWidth(): number {
49
+ return typeof window === 'undefined' ? DEFAULT_MOBILE_BREAKPOINT + 256 : window.innerWidth
50
+ }
51
+
52
+ function readHeight(): number {
53
+ return typeof window === 'undefined' ? 768 : window.innerHeight
54
+ }