@lingxia/elements 0.5.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/dist/component.d.ts +50 -0
- package/dist/component.js +139 -0
- package/dist/dom.d.ts +10 -0
- package/dist/dom.js +70 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/input.d.ts +117 -0
- package/dist/input.js +699 -0
- package/dist/native_component_wrapper_shared.d.ts +74 -0
- package/dist/native_component_wrapper_shared.js +193 -0
- package/dist/nativecomponent.d.ts +6 -0
- package/dist/nativecomponent.js +82 -0
- package/dist/navigator.d.ts +83 -0
- package/dist/navigator.js +388 -0
- package/dist/picker-web.d.ts +2 -0
- package/dist/picker-web.js +727 -0
- package/dist/picker.d.ts +77 -0
- package/dist/picker.js +342 -0
- package/dist/platform.d.ts +6 -0
- package/dist/platform.js +24 -0
- package/dist/text_component_native_attrs.d.ts +53 -0
- package/dist/text_component_native_attrs.js +111 -0
- package/dist/text_component_shared.d.ts +7 -0
- package/dist/text_component_shared.js +148 -0
- package/dist/textarea.d.ts +129 -0
- package/dist/textarea.js +841 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +1 -0
- package/dist/video.d.ts +134 -0
- package/dist/video.js +741 -0
- package/package.json +39 -0
- package/src/component.ts +164 -0
- package/src/dom.ts +83 -0
- package/src/index.ts +69 -0
- package/src/input.ts +799 -0
- package/src/native_component_wrapper_shared.ts +243 -0
- package/src/nativecomponent.ts +97 -0
- package/src/navigator.ts +533 -0
- package/src/picker-web.ts +719 -0
- package/src/picker.ts +418 -0
- package/src/platform.ts +25 -0
- package/src/text_component_native_attrs.ts +137 -0
- package/src/text_component_shared.ts +147 -0
- package/src/textarea.ts +959 -0
- package/src/types.ts +20 -0
- package/src/video.ts +855 -0
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lingxia/elements",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "LingXia custom elements — native-backed web components for LxApp pages",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"clean": "rm -rf dist",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@lingxia/bridge": "file:../lingxia-bridge",
|
|
23
|
+
"typescript": "^5.7.2"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"registry": "https://registry.npmjs.org",
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/LingXia-Dev/LingXia.git",
|
|
36
|
+
"directory": "packages/lingxia-elements"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT"
|
|
39
|
+
}
|
package/src/component.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { isIOS } from "./platform.js";
|
|
2
|
+
|
|
3
|
+
export type Rect = { x: number; y: number; width: number; height: number };
|
|
4
|
+
|
|
5
|
+
let autoId = 0;
|
|
6
|
+
|
|
7
|
+
export function ensureComponentId(el: HTMLElement, prefix: string, currentId?: string | null): string {
|
|
8
|
+
const attrId = el.getAttribute("id");
|
|
9
|
+
if (attrId && attrId.length > 0) {
|
|
10
|
+
return attrId;
|
|
11
|
+
}
|
|
12
|
+
if (currentId && currentId.length > 0) {
|
|
13
|
+
return currentId;
|
|
14
|
+
}
|
|
15
|
+
autoId += 1;
|
|
16
|
+
const id = `${prefix}-${Date.now().toString(36)}-${autoId.toString(36)}`;
|
|
17
|
+
el.setAttribute("id", id);
|
|
18
|
+
return id;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* iOS Native Component Rendering Helper.
|
|
23
|
+
* Creates a scroll container that triggers WKChildScrollView creation.
|
|
24
|
+
* Reusable by all native components (video, map, camera, etc.)
|
|
25
|
+
*/
|
|
26
|
+
export class iOSNativeComponentHelper {
|
|
27
|
+
private scrollContainer: HTMLDivElement | null = null;
|
|
28
|
+
private host: HTMLElement;
|
|
29
|
+
private componentId: string;
|
|
30
|
+
|
|
31
|
+
constructor(host: HTMLElement, componentId: string) {
|
|
32
|
+
this.host = host;
|
|
33
|
+
this.componentId = componentId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Setup scroll container for iOS. No-op on other platforms.
|
|
38
|
+
*/
|
|
39
|
+
setup(): void {
|
|
40
|
+
if (!isIOS() || this.scrollContainer) return;
|
|
41
|
+
|
|
42
|
+
const container = document.createElement("div");
|
|
43
|
+
container.id = `${this.componentId}-scroll-container`;
|
|
44
|
+
container.style.cssText = `
|
|
45
|
+
position: absolute;
|
|
46
|
+
top: 0;
|
|
47
|
+
left: 0;
|
|
48
|
+
width: 100%;
|
|
49
|
+
height: 100%;
|
|
50
|
+
overflow: scroll;
|
|
51
|
+
-webkit-overflow-scrolling: touch;
|
|
52
|
+
pointer-events: none;
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
// Add inner content larger than container to trigger WKChildScrollView creation
|
|
56
|
+
const inner = document.createElement("div");
|
|
57
|
+
inner.style.cssText = "width: 200%; height: 200%;";
|
|
58
|
+
container.appendChild(inner);
|
|
59
|
+
|
|
60
|
+
// Disable scrolling but keep overflow:scroll to trigger WKChildScrollView
|
|
61
|
+
const touchMoveListener: EventListenerObject = {
|
|
62
|
+
handleEvent: (event: Event): void => {
|
|
63
|
+
(event as TouchEvent).preventDefault();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const scrollLockListener: EventListenerObject = {
|
|
67
|
+
handleEvent: (): void => {
|
|
68
|
+
container.scrollTop = 0;
|
|
69
|
+
container.scrollLeft = 0;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
container.addEventListener('touchmove', touchMoveListener, { passive: false });
|
|
73
|
+
container.addEventListener('scroll', scrollLockListener);
|
|
74
|
+
|
|
75
|
+
this.scrollContainer = container;
|
|
76
|
+
this.host.appendChild(container);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Cleanup scroll container.
|
|
81
|
+
*/
|
|
82
|
+
cleanup(): void {
|
|
83
|
+
if (this.scrollContainer && this.host.contains(this.scrollContainer)) {
|
|
84
|
+
this.host.removeChild(this.scrollContainer);
|
|
85
|
+
}
|
|
86
|
+
this.scrollContainer = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the rect to send to native for WKChildScrollView lookup.
|
|
91
|
+
* Returns null on non-iOS platforms.
|
|
92
|
+
*/
|
|
93
|
+
getScrollContainerRect(): Rect | null {
|
|
94
|
+
if (!this.scrollContainer) return null;
|
|
95
|
+
const rect = this.scrollContainer.getBoundingClientRect();
|
|
96
|
+
return {
|
|
97
|
+
x: rect.left,
|
|
98
|
+
y: rect.top,
|
|
99
|
+
width: rect.width,
|
|
100
|
+
height: rect.height
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Enhance payload with iOS-specific data.
|
|
106
|
+
* Call this before sending mount/update messages.
|
|
107
|
+
*/
|
|
108
|
+
enhancePayload(payload: Record<string, unknown>): void {
|
|
109
|
+
const rect = this.getScrollContainerRect();
|
|
110
|
+
if (rect) {
|
|
111
|
+
payload.scrollContainerRect = rect;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function rectEquals(a: Rect | null, b: Rect, epsilon = 0): boolean {
|
|
117
|
+
if (!a) return false;
|
|
118
|
+
return (
|
|
119
|
+
Math.abs(a.x - b.x) <= epsilon &&
|
|
120
|
+
Math.abs(a.y - b.y) <= epsilon &&
|
|
121
|
+
Math.abs(a.width - b.width) <= epsilon &&
|
|
122
|
+
Math.abs(a.height - b.height) <= epsilon
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class NativeComponentUpdateState {
|
|
127
|
+
private lastPropsJson: string | null = null;
|
|
128
|
+
private lastRect: Rect | null = null;
|
|
129
|
+
private lastZIndex: number | null = null;
|
|
130
|
+
|
|
131
|
+
reset() {
|
|
132
|
+
this.lastPropsJson = null;
|
|
133
|
+
this.lastRect = null;
|
|
134
|
+
this.lastZIndex = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
decide(
|
|
138
|
+
rect: Rect,
|
|
139
|
+
props: Record<string, unknown> | null,
|
|
140
|
+
zIndex: number,
|
|
141
|
+
force = false
|
|
142
|
+
): { shouldSend: boolean; propsChanged: boolean; rectChanged: boolean; zChanged: boolean } {
|
|
143
|
+
const propsJson = props === null ? this.lastPropsJson : JSON.stringify(props);
|
|
144
|
+
const rectChanged = !rectEquals(this.lastRect, rect);
|
|
145
|
+
const propsChanged = props === null ? false : propsJson !== this.lastPropsJson || force;
|
|
146
|
+
const zChanged = this.lastZIndex !== zIndex;
|
|
147
|
+
|
|
148
|
+
const shouldSend = force || rectChanged || propsChanged || zChanged;
|
|
149
|
+
if (!shouldSend) {
|
|
150
|
+
return { shouldSend: false, propsChanged: false, rectChanged: false, zChanged: false };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (props !== null) {
|
|
154
|
+
this.lastPropsJson = propsJson;
|
|
155
|
+
}
|
|
156
|
+
this.lastRect = rect;
|
|
157
|
+
this.lastZIndex = zIndex;
|
|
158
|
+
return { shouldSend: true, propsChanged, rectChanged, zChanged };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
shouldSend(rect: Rect, props: Record<string, unknown> | null, zIndex: number, force = false): boolean {
|
|
162
|
+
return this.decide(rect, props, zIndex, force).shouldSend;
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/dom.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type MeasuredElement = {
|
|
2
|
+
rect: { x: number; y: number; width: number; height: number };
|
|
3
|
+
cornerRadius?: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function measureElement(el: HTMLElement): MeasuredElement {
|
|
7
|
+
const rect = el.getBoundingClientRect();
|
|
8
|
+
|
|
9
|
+
const parseRadius = (radiusStr: string): number | undefined => {
|
|
10
|
+
const match = radiusStr && radiusStr.match(/^([0-9.]+)px/);
|
|
11
|
+
if (!match) return undefined;
|
|
12
|
+
const parsed = parseFloat(match[1]);
|
|
13
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const isOverflowClipping = (style: CSSStyleDeclaration): boolean =>
|
|
17
|
+
style.overflow === "hidden" ||
|
|
18
|
+
style.overflow === "clip" ||
|
|
19
|
+
style.overflowX === "hidden" ||
|
|
20
|
+
style.overflowX === "clip" ||
|
|
21
|
+
style.overflowY === "hidden" ||
|
|
22
|
+
style.overflowY === "clip";
|
|
23
|
+
|
|
24
|
+
let cornerRadius = parseRadius(getComputedStyle(el).borderRadius);
|
|
25
|
+
if (cornerRadius === undefined) {
|
|
26
|
+
const rectMatches = (a: DOMRect, b: DOMRect, epsilon = 0.5) =>
|
|
27
|
+
Math.abs(a.left - b.left) <= epsilon &&
|
|
28
|
+
Math.abs(a.top - b.top) <= epsilon &&
|
|
29
|
+
Math.abs(a.width - b.width) <= epsilon &&
|
|
30
|
+
Math.abs(a.height - b.height) <= epsilon;
|
|
31
|
+
|
|
32
|
+
let parent = el.parentElement;
|
|
33
|
+
while (parent) {
|
|
34
|
+
const style = getComputedStyle(parent);
|
|
35
|
+
if (isOverflowClipping(style) && rectMatches(parent.getBoundingClientRect(), rect)) {
|
|
36
|
+
const parentRadius = parseRadius(style.borderRadius);
|
|
37
|
+
if (parentRadius !== undefined) {
|
|
38
|
+
cornerRadius = parentRadius;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
parent = parent.parentElement;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (cornerRadius === undefined) {
|
|
47
|
+
// Common card layout: native video is clipped by an overflow-hidden rounded ancestor
|
|
48
|
+
// that doesn't have exactly the same rect (e.g. header + media block).
|
|
49
|
+
const near = (a: number, b: number, epsilon = 0.75) => Math.abs(a - b) <= epsilon;
|
|
50
|
+
let parent = el.parentElement;
|
|
51
|
+
while (parent) {
|
|
52
|
+
const style = getComputedStyle(parent);
|
|
53
|
+
const parentRadius = parseRadius(style.borderRadius);
|
|
54
|
+
if (isOverflowClipping(style) && parentRadius !== undefined) {
|
|
55
|
+
const p = parent.getBoundingClientRect();
|
|
56
|
+
const contains =
|
|
57
|
+
rect.left >= p.left - 0.75 &&
|
|
58
|
+
rect.right <= p.right + 0.75 &&
|
|
59
|
+
rect.top >= p.top - 0.75 &&
|
|
60
|
+
rect.bottom <= p.bottom + 0.75;
|
|
61
|
+
const alignedLeftRight = near(rect.left, p.left) && near(rect.right, p.right);
|
|
62
|
+
const touchesTopOrBottom = near(rect.top, p.top) || near(rect.bottom, p.bottom);
|
|
63
|
+
if (contains && alignedLeftRight && touchesTopOrBottom) {
|
|
64
|
+
cornerRadius = parentRadius;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
parent = parent.parentElement;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Return document coordinates (CSS pixels)
|
|
73
|
+
// Native layer converts to physical pixels and handles scroll offset
|
|
74
|
+
return {
|
|
75
|
+
rect: {
|
|
76
|
+
x: rect.left + window.scrollX,
|
|
77
|
+
y: rect.top + window.scrollY,
|
|
78
|
+
width: rect.width,
|
|
79
|
+
height: rect.height
|
|
80
|
+
},
|
|
81
|
+
cornerRadius
|
|
82
|
+
};
|
|
83
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export {
|
|
2
|
+
registerVideoComponent,
|
|
3
|
+
LxVideoElement,
|
|
4
|
+
type LxVideoAttributes,
|
|
5
|
+
} from "./video.js";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
registerPickerComponent,
|
|
9
|
+
LxPickerElement,
|
|
10
|
+
type LxPickerAttributes,
|
|
11
|
+
type LxPickerColumn,
|
|
12
|
+
type LxPickerCascadingColumns,
|
|
13
|
+
type LxPickerEvent,
|
|
14
|
+
type LxPickerEventDetail,
|
|
15
|
+
} from "./picker.js";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
registerInputComponent,
|
|
19
|
+
LxInputElement,
|
|
20
|
+
type LxInputAttributes,
|
|
21
|
+
type LxInputEvent,
|
|
22
|
+
type LxInputEventDetail,
|
|
23
|
+
} from "./input.js";
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
registerTextareaComponent,
|
|
27
|
+
LxTextareaElement,
|
|
28
|
+
type LxTextareaAttributes,
|
|
29
|
+
type LxTextareaEvent,
|
|
30
|
+
type LxTextareaEventDetail,
|
|
31
|
+
} from "./textarea.js";
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
LxNavigatorElement,
|
|
35
|
+
type LxNavigatorAttributes,
|
|
36
|
+
type LxNavigatorEvent,
|
|
37
|
+
type NavigatorOpenType,
|
|
38
|
+
type NavigatorTarget,
|
|
39
|
+
} from "./navigator.js";
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
buildVideoNativeAttrs,
|
|
43
|
+
buildNavigatorNativeAttrs,
|
|
44
|
+
buildPickerNativeAttrs,
|
|
45
|
+
getPickerDisplayText,
|
|
46
|
+
getPickerValueFromIndex,
|
|
47
|
+
VIDEO_DOM_EVENT_MAP,
|
|
48
|
+
} from "./native_component_wrapper_shared.js";
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
buildInputNativeAttrs,
|
|
52
|
+
buildTextareaNativeAttrs,
|
|
53
|
+
} from "./text_component_native_attrs.js";
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
isHarmony,
|
|
57
|
+
isIOS,
|
|
58
|
+
isAndroid,
|
|
59
|
+
isMacOS,
|
|
60
|
+
isDesktop,
|
|
61
|
+
getOS,
|
|
62
|
+
} from "./platform.js";
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
ensureComponentId,
|
|
66
|
+
rectEquals,
|
|
67
|
+
NativeComponentUpdateState,
|
|
68
|
+
type Rect,
|
|
69
|
+
} from "./component.js";
|