@lingxia/bridge 0.5.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/dist/bridge-runtime.es2020.js +1 -0
- package/dist/bridge-runtime.es5.js +1 -0
- package/dist/bridge.d.ts +5 -2
- package/dist/es2020/boot.js +34 -0
- package/dist/es2020/bridge.js +1597 -0
- package/dist/es2020/error.js +89 -0
- package/dist/es2020/image-proxy.js +82 -0
- package/dist/es2020/index.js +19 -0
- package/dist/es2020/invocation.js +77 -0
- package/dist/es2020/nativecomponents/coverage-monitor.js +159 -0
- package/dist/es2020/runtime-env.js +31 -0
- package/dist/es2020/types.js +16 -0
- package/dist/es2020/ui/action-sheet.js +138 -0
- package/dist/es2020/ui/index.js +10 -0
- package/dist/es2020/ui/modal.js +170 -0
- package/dist/es2020/ui/toast.js +145 -0
- package/dist/es5/boot.js +34 -0
- package/dist/es5/bridge.js +1930 -0
- package/dist/es5/error.js +35 -0
- package/dist/es5/image-proxy.js +83 -0
- package/dist/es5/index.js +19 -0
- package/dist/es5/invocation.js +103 -0
- package/dist/es5/nativecomponents/coverage-monitor.js +219 -0
- package/dist/es5/runtime-env.js +32 -0
- package/dist/es5/types.js +16 -0
- package/dist/es5/ui/action-sheet.js +82 -0
- package/dist/es5/ui/index.js +10 -0
- package/dist/es5/ui/modal.js +84 -0
- package/dist/es5/ui/toast.js +102 -0
- package/dist/index.d.ts +3 -2
- package/dist/invocation.d.ts +14 -0
- package/dist/types.d.ts +42 -15
- package/package.json +21 -9
- package/dist/runtime.es2020.js +0 -1663
- package/dist/runtime.es5.js +0 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const ERROR_STYLES = `
|
|
2
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3
|
+
body {
|
|
4
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, sans-serif;
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
min-height: 100vh;
|
|
9
|
+
background: #f5f5f7;
|
|
10
|
+
color: #1d1d1f;
|
|
11
|
+
padding: 20px;
|
|
12
|
+
-webkit-font-smoothing: antialiased;
|
|
13
|
+
}
|
|
14
|
+
.lx-error {
|
|
15
|
+
text-align: center;
|
|
16
|
+
max-width: 400px;
|
|
17
|
+
}
|
|
18
|
+
.lx-error-code {
|
|
19
|
+
font-size: 120px;
|
|
20
|
+
font-weight: 700;
|
|
21
|
+
color: #d1d1d6;
|
|
22
|
+
line-height: 1;
|
|
23
|
+
letter-spacing: -4px;
|
|
24
|
+
}
|
|
25
|
+
.lx-error-title {
|
|
26
|
+
font-size: 21px;
|
|
27
|
+
font-weight: 600;
|
|
28
|
+
margin: 16px 0 8px;
|
|
29
|
+
color: #1d1d1f;
|
|
30
|
+
}
|
|
31
|
+
.lx-error-desc {
|
|
32
|
+
font-size: 15px;
|
|
33
|
+
color: #86868b;
|
|
34
|
+
line-height: 1.5;
|
|
35
|
+
}
|
|
36
|
+
.lx-error-path {
|
|
37
|
+
display: inline-block;
|
|
38
|
+
margin-top: 20px;
|
|
39
|
+
padding: 10px 16px;
|
|
40
|
+
background: rgba(0,0,0,0.04);
|
|
41
|
+
border-radius: 8px;
|
|
42
|
+
font-family: 'SF Mono', ui-monospace, Menlo, Monaco, monospace;
|
|
43
|
+
font-size: 13px;
|
|
44
|
+
color: #1d1d1f;
|
|
45
|
+
word-break: break-all;
|
|
46
|
+
max-width: 100%;
|
|
47
|
+
}
|
|
48
|
+
@media (prefers-color-scheme: dark) {
|
|
49
|
+
body { background: #000; color: #f5f5f7; }
|
|
50
|
+
.lx-error-code { color: #3a3a3c; }
|
|
51
|
+
.lx-error-title { color: #f5f5f7; }
|
|
52
|
+
.lx-error-desc { color: #86868b; }
|
|
53
|
+
.lx-error-path { background: rgba(255,255,255,0.08); color: #f5f5f7; }
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
function escapeHtml(str) {
|
|
57
|
+
const div = document.createElement('div');
|
|
58
|
+
div.textContent = str;
|
|
59
|
+
return div.innerHTML;
|
|
60
|
+
}
|
|
61
|
+
export function renderErrorUI(errorInfo) {
|
|
62
|
+
const { failedPath, reason } = errorInfo;
|
|
63
|
+
const styleEl = document.createElement('style');
|
|
64
|
+
styleEl.textContent = ERROR_STYLES;
|
|
65
|
+
document.head.appendChild(styleEl);
|
|
66
|
+
document.body.innerHTML = '';
|
|
67
|
+
document.body.style.cssText = '';
|
|
68
|
+
const container = document.createElement('div');
|
|
69
|
+
container.className = 'lx-error';
|
|
70
|
+
let html = '<div class="lx-error-code">404</div>';
|
|
71
|
+
html += '<h1 class="lx-error-title">Page Not Found</h1>';
|
|
72
|
+
html += '<p class="lx-error-desc">The page you\'re looking for doesn\'t exist.</p>';
|
|
73
|
+
if (failedPath) {
|
|
74
|
+
html += `<div class="lx-error-path">${escapeHtml(failedPath)}</div>`;
|
|
75
|
+
}
|
|
76
|
+
if (reason) {
|
|
77
|
+
html += `<p class="lx-error-desc" style="margin-top:12px">${escapeHtml(reason)}</p>`;
|
|
78
|
+
}
|
|
79
|
+
container.innerHTML = html;
|
|
80
|
+
document.body.appendChild(container);
|
|
81
|
+
}
|
|
82
|
+
export function hasError() {
|
|
83
|
+
const config = window.__LX_RUNTIME_CONFIG;
|
|
84
|
+
return !!(config && config.error);
|
|
85
|
+
}
|
|
86
|
+
export function getErrorInfo() {
|
|
87
|
+
const config = window.__LX_RUNTIME_CONFIG;
|
|
88
|
+
return config?.error || null;
|
|
89
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { getPlatformOS } from './runtime-env';
|
|
2
|
+
const PROXY_PREFIX = 'lx://proxy/';
|
|
3
|
+
function shouldProxyImageUrl(trimmed) {
|
|
4
|
+
if (trimmed.length === 0) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
if (trimmed.startsWith('lx://') ||
|
|
8
|
+
trimmed.startsWith('data:') ||
|
|
9
|
+
trimmed.startsWith('blob:') ||
|
|
10
|
+
trimmed.startsWith('file:')) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return trimmed.startsWith('http://') || trimmed.startsWith('https://');
|
|
14
|
+
}
|
|
15
|
+
function toProxyUrl(url) {
|
|
16
|
+
const b64 = btoa(url);
|
|
17
|
+
const urlSafe = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
18
|
+
return `${PROXY_PREFIX}${urlSafe}`;
|
|
19
|
+
}
|
|
20
|
+
function proxyImageUrl(url) {
|
|
21
|
+
const trimmed = url.trim();
|
|
22
|
+
if (!shouldProxyImageUrl(trimmed))
|
|
23
|
+
return url;
|
|
24
|
+
return toProxyUrl(trimmed);
|
|
25
|
+
}
|
|
26
|
+
export function setupImageProxy() {
|
|
27
|
+
const os = getPlatformOS();
|
|
28
|
+
if (os !== 'iOS' && os !== 'macOS')
|
|
29
|
+
return;
|
|
30
|
+
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
31
|
+
return;
|
|
32
|
+
if (typeof HTMLImageElement === 'undefined')
|
|
33
|
+
return;
|
|
34
|
+
const proto = HTMLImageElement.prototype;
|
|
35
|
+
if (proto.__lxProxyPatched)
|
|
36
|
+
return;
|
|
37
|
+
proto.__lxProxyPatched = true;
|
|
38
|
+
const originalSetAttribute = proto.setAttribute;
|
|
39
|
+
proto.setAttribute = function (name, value) {
|
|
40
|
+
if (typeof name === 'string' && name.toLowerCase() === 'src' && typeof value === 'string') {
|
|
41
|
+
return originalSetAttribute.call(this, name, proxyImageUrl(value));
|
|
42
|
+
}
|
|
43
|
+
return originalSetAttribute.call(this, name, value);
|
|
44
|
+
};
|
|
45
|
+
const srcDescriptor = Object.getOwnPropertyDescriptor(proto, 'src');
|
|
46
|
+
if (srcDescriptor?.set) {
|
|
47
|
+
Object.defineProperty(proto, 'src', {
|
|
48
|
+
configurable: srcDescriptor.configurable,
|
|
49
|
+
enumerable: srcDescriptor.enumerable,
|
|
50
|
+
get: srcDescriptor.get,
|
|
51
|
+
set(value) {
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
srcDescriptor.set?.call(this, proxyImageUrl(value));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
srcDescriptor.set?.call(this, value);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const rewriteExisting = () => {
|
|
62
|
+
const imgs = document.querySelectorAll('img[src]');
|
|
63
|
+
imgs.forEach((img) => {
|
|
64
|
+
const src = img.getAttribute('src');
|
|
65
|
+
if (!src)
|
|
66
|
+
return;
|
|
67
|
+
const proxied = proxyImageUrl(src);
|
|
68
|
+
if (proxied !== src) {
|
|
69
|
+
originalSetAttribute.call(img, 'src', proxied);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
const domReadyListener = {
|
|
74
|
+
handleEvent: () => rewriteExisting(),
|
|
75
|
+
};
|
|
76
|
+
if (document.readyState === 'loading') {
|
|
77
|
+
document.addEventListener('DOMContentLoaded', domReadyListener);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
rewriteExisting();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LingXia Web Runtime
|
|
3
|
+
*
|
|
4
|
+
* Injected into SPA HTML to handle:
|
|
5
|
+
* - Bridge initialization for native communication
|
|
6
|
+
* - Error UI rendering when page cannot be loaded
|
|
7
|
+
*/
|
|
8
|
+
export { LingXiaBridge, channel, initBridge, invoke, notify, stream, } from './bridge';
|
|
9
|
+
export { isNativeError } from './invocation';
|
|
10
|
+
export { renderErrorUI, hasError, getErrorInfo } from './error';
|
|
11
|
+
export { boot, bootWhenReady } from './boot';
|
|
12
|
+
import { bootWhenReady } from './boot';
|
|
13
|
+
import { setupImageProxy } from './image-proxy';
|
|
14
|
+
import { registerUIHandlers } from './ui';
|
|
15
|
+
if (__LX_RUNTIME_PLATFORM__ === 'all' || __LX_RUNTIME_PLATFORM__ === 'desktop') {
|
|
16
|
+
registerUIHandlers();
|
|
17
|
+
}
|
|
18
|
+
setupImageProxy();
|
|
19
|
+
bootWhenReady();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { BRIDGE_ERROR } from "./types";
|
|
2
|
+
class BridgeInvocationError extends Error {
|
|
3
|
+
constructor(code, message, data) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "BridgeInvocationError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.data = data;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function toBridgeError(err) {
|
|
11
|
+
if (err instanceof BridgeInvocationError)
|
|
12
|
+
return err;
|
|
13
|
+
if (err && typeof err === "object") {
|
|
14
|
+
const source = err;
|
|
15
|
+
let code = BRIDGE_ERROR.INTERNAL_ERROR;
|
|
16
|
+
if (typeof source.code === "string" && source.code.trim() !== "") {
|
|
17
|
+
code = source.code;
|
|
18
|
+
}
|
|
19
|
+
else if (typeof source.code === "number" && Number.isFinite(source.code)) {
|
|
20
|
+
code = source.code;
|
|
21
|
+
}
|
|
22
|
+
const message = typeof source.message === "string" && source.message.trim() !== ""
|
|
23
|
+
? source.message
|
|
24
|
+
: "Unknown error";
|
|
25
|
+
return new BridgeInvocationError(code, message, "data" in source ? source.data : undefined);
|
|
26
|
+
}
|
|
27
|
+
const message = err instanceof Error ? err.message : typeof err === "string" ? err : "Unknown error";
|
|
28
|
+
return new BridgeInvocationError(BRIDGE_ERROR.INTERNAL_ERROR, message);
|
|
29
|
+
}
|
|
30
|
+
export function toNativeError(err) {
|
|
31
|
+
const bridgeError = toBridgeError(err);
|
|
32
|
+
return {
|
|
33
|
+
code: String(bridgeError.code || BRIDGE_ERROR.INTERNAL_ERROR),
|
|
34
|
+
message: typeof bridgeError.message === "string" && bridgeError.message.trim() !== ""
|
|
35
|
+
? bridgeError.message
|
|
36
|
+
: "Unknown error",
|
|
37
|
+
...(bridgeError.data === undefined ? {} : { data: bridgeError.data }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function isNativeError(error, code) {
|
|
41
|
+
if (!error || typeof error !== "object")
|
|
42
|
+
return false;
|
|
43
|
+
const candidate = error;
|
|
44
|
+
if (typeof candidate.code !== "string" || candidate.code.trim() === "")
|
|
45
|
+
return false;
|
|
46
|
+
if (typeof candidate.message !== "string")
|
|
47
|
+
return false;
|
|
48
|
+
return code === undefined || candidate.code === code;
|
|
49
|
+
}
|
|
50
|
+
export function resolveParams(source) {
|
|
51
|
+
if (typeof source === "function") {
|
|
52
|
+
return source();
|
|
53
|
+
}
|
|
54
|
+
return source;
|
|
55
|
+
}
|
|
56
|
+
export function stableParamKey(value) {
|
|
57
|
+
if (value === undefined)
|
|
58
|
+
return "undefined";
|
|
59
|
+
try {
|
|
60
|
+
return JSON.stringify(value) ?? "undefined";
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return String(value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function invokeMethod(method, params) {
|
|
67
|
+
if (params === undefined) {
|
|
68
|
+
return method();
|
|
69
|
+
}
|
|
70
|
+
return method(params);
|
|
71
|
+
}
|
|
72
|
+
export function getMethodKey(method) {
|
|
73
|
+
if (typeof method !== "function")
|
|
74
|
+
return undefined;
|
|
75
|
+
const candidate = method.__funcName;
|
|
76
|
+
return typeof candidate === "string" && candidate !== "" ? candidate : undefined;
|
|
77
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export function installNativeComponentCoverageMonitor({ os, send }) {
|
|
2
|
+
if (typeof window === 'undefined')
|
|
3
|
+
return;
|
|
4
|
+
if (typeof document === 'undefined')
|
|
5
|
+
return;
|
|
6
|
+
const isIOS = os === 'iOS';
|
|
7
|
+
const isAndroid = os === 'Android';
|
|
8
|
+
if (!isIOS && !isAndroid)
|
|
9
|
+
return;
|
|
10
|
+
const key = Symbol.for('LingXia.NativeComponentCoverageMonitor');
|
|
11
|
+
const existing = window[key];
|
|
12
|
+
if (existing?.installed)
|
|
13
|
+
return;
|
|
14
|
+
const state = {
|
|
15
|
+
installed: true,
|
|
16
|
+
coveredById: new Map(),
|
|
17
|
+
coveredCount: 0,
|
|
18
|
+
anyCovered: false,
|
|
19
|
+
scheduled: false,
|
|
20
|
+
hasAnyVideo: false,
|
|
21
|
+
scrollLocked: false,
|
|
22
|
+
scrollY: 0,
|
|
23
|
+
htmlOverflow: '',
|
|
24
|
+
bodyOverflow: '',
|
|
25
|
+
bodyPosition: '',
|
|
26
|
+
bodyTop: '',
|
|
27
|
+
bodyWidth: '',
|
|
28
|
+
};
|
|
29
|
+
window[key] = state;
|
|
30
|
+
function lockScroll() {
|
|
31
|
+
if (!isIOS)
|
|
32
|
+
return;
|
|
33
|
+
if (state.scrollLocked)
|
|
34
|
+
return;
|
|
35
|
+
const html = document.documentElement;
|
|
36
|
+
const body = document.body;
|
|
37
|
+
if (!html || !body)
|
|
38
|
+
return;
|
|
39
|
+
state.scrollLocked = true;
|
|
40
|
+
state.scrollY = window.scrollY || html.scrollTop || 0;
|
|
41
|
+
state.htmlOverflow = html.style.overflow || '';
|
|
42
|
+
state.bodyOverflow = body.style.overflow || '';
|
|
43
|
+
state.bodyPosition = body.style.position || '';
|
|
44
|
+
state.bodyTop = body.style.top || '';
|
|
45
|
+
state.bodyWidth = body.style.width || '';
|
|
46
|
+
html.style.overflow = 'hidden';
|
|
47
|
+
body.style.overflow = 'hidden';
|
|
48
|
+
body.style.position = 'fixed';
|
|
49
|
+
body.style.top = `${-state.scrollY}px`;
|
|
50
|
+
body.style.width = '100%';
|
|
51
|
+
}
|
|
52
|
+
function unlockScroll() {
|
|
53
|
+
if (!isIOS)
|
|
54
|
+
return;
|
|
55
|
+
if (!state.scrollLocked)
|
|
56
|
+
return;
|
|
57
|
+
const html = document.documentElement;
|
|
58
|
+
const body = document.body;
|
|
59
|
+
if (!html || !body)
|
|
60
|
+
return;
|
|
61
|
+
state.scrollLocked = false;
|
|
62
|
+
html.style.overflow = state.htmlOverflow;
|
|
63
|
+
body.style.overflow = state.bodyOverflow;
|
|
64
|
+
body.style.position = state.bodyPosition;
|
|
65
|
+
body.style.top = state.bodyTop;
|
|
66
|
+
body.style.width = state.bodyWidth;
|
|
67
|
+
window.scrollTo(0, state.scrollY);
|
|
68
|
+
}
|
|
69
|
+
function isElementCovered(el) {
|
|
70
|
+
const rect = el.getBoundingClientRect?.();
|
|
71
|
+
if (!rect)
|
|
72
|
+
return false;
|
|
73
|
+
if (rect.width < 2 || rect.height < 2)
|
|
74
|
+
return false;
|
|
75
|
+
const x = rect.left + rect.width / 2;
|
|
76
|
+
const y = rect.top + rect.height / 2;
|
|
77
|
+
if (x < 0 || y < 0 || x >= window.innerWidth || y >= window.innerHeight)
|
|
78
|
+
return false;
|
|
79
|
+
const top = document.elementFromPoint?.(x, y);
|
|
80
|
+
if (!top)
|
|
81
|
+
return false;
|
|
82
|
+
if (top === document.documentElement || top === document.body)
|
|
83
|
+
return false;
|
|
84
|
+
return !(top === el || el.contains(top));
|
|
85
|
+
}
|
|
86
|
+
function check() {
|
|
87
|
+
state.scheduled = false;
|
|
88
|
+
const nodes = document.querySelectorAll('lx-video[id]');
|
|
89
|
+
state.hasAnyVideo = nodes.length > 0;
|
|
90
|
+
if (!state.hasAnyVideo && state.coveredById.size === 0 && !state.anyCovered) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const seenIds = new Set();
|
|
94
|
+
for (const node of Array.from(nodes)) {
|
|
95
|
+
const id = node.id;
|
|
96
|
+
if (!id)
|
|
97
|
+
continue;
|
|
98
|
+
seenIds.add(id);
|
|
99
|
+
const covered = isElementCovered(node);
|
|
100
|
+
const prev = state.coveredById.get(id);
|
|
101
|
+
if (prev === covered)
|
|
102
|
+
continue;
|
|
103
|
+
if (prev === true)
|
|
104
|
+
state.coveredCount -= 1;
|
|
105
|
+
if (covered === true)
|
|
106
|
+
state.coveredCount += 1;
|
|
107
|
+
state.coveredById.set(id, covered);
|
|
108
|
+
send({ id, action: 'component.coverage', covered });
|
|
109
|
+
}
|
|
110
|
+
const staleIds = [];
|
|
111
|
+
for (const [id, covered] of state.coveredById.entries()) {
|
|
112
|
+
if (seenIds.has(id))
|
|
113
|
+
continue;
|
|
114
|
+
staleIds.push(id);
|
|
115
|
+
if (covered === true)
|
|
116
|
+
state.coveredCount -= 1;
|
|
117
|
+
}
|
|
118
|
+
for (const id of staleIds) {
|
|
119
|
+
state.coveredById.delete(id);
|
|
120
|
+
send({ id, action: 'component.coverage', covered: false });
|
|
121
|
+
}
|
|
122
|
+
const anyCovered = state.coveredCount > 0;
|
|
123
|
+
if (anyCovered !== state.anyCovered) {
|
|
124
|
+
state.anyCovered = anyCovered;
|
|
125
|
+
if (isIOS) {
|
|
126
|
+
if (anyCovered)
|
|
127
|
+
lockScroll();
|
|
128
|
+
else
|
|
129
|
+
unlockScroll();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function schedule() {
|
|
134
|
+
if (state.scheduled)
|
|
135
|
+
return;
|
|
136
|
+
state.scheduled = true;
|
|
137
|
+
requestAnimationFrame(check);
|
|
138
|
+
}
|
|
139
|
+
function onScroll() {
|
|
140
|
+
if (!state.hasAnyVideo && state.coveredById.size === 0)
|
|
141
|
+
return;
|
|
142
|
+
schedule();
|
|
143
|
+
}
|
|
144
|
+
const observer = new MutationObserver(schedule);
|
|
145
|
+
observer.observe(document.documentElement, {
|
|
146
|
+
subtree: true,
|
|
147
|
+
childList: true,
|
|
148
|
+
attributes: true,
|
|
149
|
+
});
|
|
150
|
+
const scrollListener = {
|
|
151
|
+
handleEvent: () => onScroll(),
|
|
152
|
+
};
|
|
153
|
+
const resizeListener = {
|
|
154
|
+
handleEvent: () => schedule(),
|
|
155
|
+
};
|
|
156
|
+
window.addEventListener('scroll', scrollListener, { capture: true, passive: true });
|
|
157
|
+
window.addEventListener('resize', resizeListener);
|
|
158
|
+
schedule();
|
|
159
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const BRIDGE_CONFIG = (typeof window !== 'undefined' && window.__LX_BRIDGE_CFG) || {};
|
|
2
|
+
export function getPlatformOS() {
|
|
3
|
+
return BRIDGE_CONFIG.os || 'unknown';
|
|
4
|
+
}
|
|
5
|
+
export function isHarmony() {
|
|
6
|
+
return BRIDGE_CONFIG.os === 'Harmony';
|
|
7
|
+
}
|
|
8
|
+
export function isIOS() {
|
|
9
|
+
return BRIDGE_CONFIG.os === 'iOS';
|
|
10
|
+
}
|
|
11
|
+
export function isAndroid() {
|
|
12
|
+
return BRIDGE_CONFIG.os === 'Android';
|
|
13
|
+
}
|
|
14
|
+
export function isMacOS() {
|
|
15
|
+
return BRIDGE_CONFIG.os === 'macOS';
|
|
16
|
+
}
|
|
17
|
+
export function isDesktop() {
|
|
18
|
+
return isMacOS();
|
|
19
|
+
}
|
|
20
|
+
export function getCommunicationMethod() {
|
|
21
|
+
if (BRIDGE_CONFIG.os === 'iOS' || BRIDGE_CONFIG.os === 'macOS')
|
|
22
|
+
return 'webkit';
|
|
23
|
+
if (BRIDGE_CONFIG.os === 'Harmony')
|
|
24
|
+
return 'messageport';
|
|
25
|
+
if (BRIDGE_CONFIG.os === 'Android') {
|
|
26
|
+
if (window.LingXiaProxy?.supportsMessagePort?.())
|
|
27
|
+
return 'messageport';
|
|
28
|
+
return 'jsinterface';
|
|
29
|
+
}
|
|
30
|
+
return 'unknown';
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// V2 Error Codes (stable)
|
|
2
|
+
export const BRIDGE_ERROR = {
|
|
3
|
+
NOT_READY: 'BRIDGE_NOT_READY',
|
|
4
|
+
TIMEOUT: 'BRIDGE_TIMEOUT',
|
|
5
|
+
CANCELED: 'BRIDGE_CANCELED',
|
|
6
|
+
PROTOCOL_MISMATCH: 'BRIDGE_PROTOCOL_MISMATCH',
|
|
7
|
+
HANDSHAKE_FAILED: 'BRIDGE_HANDSHAKE_FAILED',
|
|
8
|
+
MALFORMED_MESSAGE: 'BRIDGE_MALFORMED_MESSAGE',
|
|
9
|
+
METHOD_NOT_FOUND: 'BRIDGE_METHOD_NOT_FOUND',
|
|
10
|
+
TOPIC_NOT_FOUND: 'BRIDGE_TOPIC_NOT_FOUND',
|
|
11
|
+
CAPABILITY_DENIED: 'BRIDGE_CAPABILITY_DENIED',
|
|
12
|
+
INTERNAL_ERROR: 'BRIDGE_INTERNAL_ERROR',
|
|
13
|
+
OUTBOX_FULL: 'BRIDGE_OUTBOX_FULL',
|
|
14
|
+
STREAM_OVERFLOW: 'BRIDGE_STREAM_OVERFLOW',
|
|
15
|
+
STREAM_CLOSED: 'BRIDGE_STREAM_CLOSED',
|
|
16
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { getPlatformOS } from '../runtime-env';
|
|
2
|
+
const ANIMATION_MS = 300;
|
|
3
|
+
const STYLE_ID = 'lx-action-sheet-style';
|
|
4
|
+
let activeSheet = null;
|
|
5
|
+
function isDesktopMode() {
|
|
6
|
+
return getPlatformOS() === 'macOS';
|
|
7
|
+
}
|
|
8
|
+
function ensureActionSheetStyle() {
|
|
9
|
+
if (document.getElementById(STYLE_ID))
|
|
10
|
+
return;
|
|
11
|
+
const style = document.createElement('style');
|
|
12
|
+
style.id = STYLE_ID;
|
|
13
|
+
style.textContent = `
|
|
14
|
+
.lx-as-backdrop {
|
|
15
|
+
position: fixed; inset: 0; z-index: 99999;
|
|
16
|
+
background: rgba(0,0,0,0.4);
|
|
17
|
+
opacity: 0; transition: opacity ${ANIMATION_MS}ms ease;
|
|
18
|
+
}
|
|
19
|
+
.lx-as-backdrop.lx-as-visible { opacity: 1; }
|
|
20
|
+
|
|
21
|
+
.lx-as-sheet {
|
|
22
|
+
position: fixed; left: 0; right: 0; bottom: 0; z-index: 100000;
|
|
23
|
+
transform: translateY(100%);
|
|
24
|
+
transition: transform ${ANIMATION_MS}ms ease;
|
|
25
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, sans-serif;
|
|
26
|
+
-webkit-font-smoothing: antialiased;
|
|
27
|
+
}
|
|
28
|
+
.lx-as-sheet.lx-as-visible { transform: translateY(0); }
|
|
29
|
+
.lx-as-sheet.lx-as-desktop {
|
|
30
|
+
left: 50%;
|
|
31
|
+
right: auto;
|
|
32
|
+
bottom: auto;
|
|
33
|
+
top: 50%;
|
|
34
|
+
width: min(92vw, 460px);
|
|
35
|
+
transform: translate(-50%, calc(-50% + 16px));
|
|
36
|
+
}
|
|
37
|
+
.lx-as-sheet.lx-as-desktop.lx-as-visible { transform: translate(-50%, -50%); }
|
|
38
|
+
|
|
39
|
+
.lx-as-container {
|
|
40
|
+
background: #fff;
|
|
41
|
+
border-radius: 16px 16px 0 0;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
}
|
|
44
|
+
.lx-as-container.lx-as-desktop {
|
|
45
|
+
border-radius: 16px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.lx-as-item {
|
|
49
|
+
display: block; width: 100%;
|
|
50
|
+
height: 56px;
|
|
51
|
+
line-height: 56px;
|
|
52
|
+
padding: 0;
|
|
53
|
+
border: none; background: none;
|
|
54
|
+
font-size: 18px; font-weight: 400;
|
|
55
|
+
text-align: center;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
-webkit-tap-highlight-color: transparent;
|
|
58
|
+
position: relative;
|
|
59
|
+
}
|
|
60
|
+
.lx-as-item:active { background: rgba(0,0,0,0.08); }
|
|
61
|
+
.lx-as-item + .lx-as-item::before {
|
|
62
|
+
content: ''; position: absolute; top: 0; left: 0; right: 0;
|
|
63
|
+
height: 1px; background: #E0E0E0;
|
|
64
|
+
}
|
|
65
|
+
.lx-as-separator {
|
|
66
|
+
height: 8px;
|
|
67
|
+
background: #F2F2F2;
|
|
68
|
+
}
|
|
69
|
+
.lx-as-cancel-btn { font-weight: 500; color: #000; }
|
|
70
|
+
`;
|
|
71
|
+
document.head.appendChild(style);
|
|
72
|
+
}
|
|
73
|
+
export function showActionSheet(params) {
|
|
74
|
+
const p = params;
|
|
75
|
+
const items = p?.itemList ?? [];
|
|
76
|
+
const cancelText = p?.cancelText ?? 'Cancel';
|
|
77
|
+
const itemColor = p?.itemColor ?? '#007AFF';
|
|
78
|
+
const task = (activeSheet ?? Promise.resolve()).then(() => renderSheet(items, cancelText, itemColor), () => renderSheet(items, cancelText, itemColor));
|
|
79
|
+
activeSheet = task;
|
|
80
|
+
return task;
|
|
81
|
+
}
|
|
82
|
+
function renderSheet(items, cancelText, itemColor) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
let dismissed = false;
|
|
85
|
+
const desktopMode = isDesktopMode();
|
|
86
|
+
ensureActionSheetStyle();
|
|
87
|
+
// --- DOM ---
|
|
88
|
+
const backdrop = document.createElement('div');
|
|
89
|
+
backdrop.className = 'lx-as-backdrop';
|
|
90
|
+
const sheet = document.createElement('div');
|
|
91
|
+
sheet.className = 'lx-as-sheet';
|
|
92
|
+
if (desktopMode) {
|
|
93
|
+
sheet.classList.add('lx-as-desktop');
|
|
94
|
+
}
|
|
95
|
+
const container = document.createElement('div');
|
|
96
|
+
container.className = 'lx-as-container';
|
|
97
|
+
if (desktopMode) {
|
|
98
|
+
container.classList.add('lx-as-desktop');
|
|
99
|
+
}
|
|
100
|
+
items.forEach((text, index) => {
|
|
101
|
+
const btn = document.createElement('button');
|
|
102
|
+
btn.className = 'lx-as-item';
|
|
103
|
+
btn.textContent = text;
|
|
104
|
+
btn.style.color = itemColor;
|
|
105
|
+
btn.addEventListener('click', () => dismiss(index));
|
|
106
|
+
container.appendChild(btn);
|
|
107
|
+
});
|
|
108
|
+
const separator = document.createElement('div');
|
|
109
|
+
separator.className = 'lx-as-separator';
|
|
110
|
+
container.appendChild(separator);
|
|
111
|
+
const cancelBtn = document.createElement('button');
|
|
112
|
+
cancelBtn.className = 'lx-as-item lx-as-cancel-btn';
|
|
113
|
+
cancelBtn.textContent = cancelText;
|
|
114
|
+
cancelBtn.addEventListener('click', () => dismiss(-1));
|
|
115
|
+
container.appendChild(cancelBtn);
|
|
116
|
+
sheet.appendChild(container);
|
|
117
|
+
document.body.appendChild(backdrop);
|
|
118
|
+
document.body.appendChild(sheet);
|
|
119
|
+
// Trigger enter animation on next frame
|
|
120
|
+
requestAnimationFrame(() => {
|
|
121
|
+
backdrop.classList.add('lx-as-visible');
|
|
122
|
+
sheet.classList.add('lx-as-visible');
|
|
123
|
+
});
|
|
124
|
+
backdrop.addEventListener('click', () => dismiss(-1));
|
|
125
|
+
function dismiss(tapIndex) {
|
|
126
|
+
if (dismissed)
|
|
127
|
+
return;
|
|
128
|
+
dismissed = true;
|
|
129
|
+
backdrop.classList.remove('lx-as-visible');
|
|
130
|
+
sheet.classList.remove('lx-as-visible');
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
backdrop.remove();
|
|
133
|
+
sheet.remove();
|
|
134
|
+
resolve({ tapIndex });
|
|
135
|
+
}, ANIMATION_MS);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { registerViewMethodHandler } from '../bridge';
|
|
2
|
+
import { showActionSheet } from './action-sheet';
|
|
3
|
+
import { showModal } from './modal';
|
|
4
|
+
import { showToast, hideToast } from './toast';
|
|
5
|
+
export function registerUIHandlers() {
|
|
6
|
+
registerViewMethodHandler('ui.showActionSheet', showActionSheet);
|
|
7
|
+
registerViewMethodHandler('ui.showModal', showModal);
|
|
8
|
+
registerViewMethodHandler('ui.showToast', showToast);
|
|
9
|
+
registerViewMethodHandler('ui.hideToast', hideToast);
|
|
10
|
+
}
|