@lingxia/elements 0.9.0 → 0.10.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/index.d.ts +0 -3
- package/dist/index.js +0 -3
- package/dist/platform.d.ts +1 -0
- package/dist/platform.js +4 -0
- package/dist/video.d.ts +3 -0
- package/dist/video.js +77 -1
- package/package.json +1 -1
- package/src/index.ts +0 -20
- package/src/platform.ts +4 -0
- package/src/video.ts +77 -1
- package/dist/input.d.ts +0 -117
- package/dist/input.js +0 -699
- package/dist/text_component_native_attrs.d.ts +0 -53
- package/dist/text_component_native_attrs.js +0 -111
- package/dist/text_component_shared.d.ts +0 -7
- package/dist/text_component_shared.js +0 -148
- package/dist/textarea.d.ts +0 -129
- package/dist/textarea.js +0 -841
- package/src/input.ts +0 -799
- package/src/text_component_native_attrs.ts +0 -137
- package/src/text_component_shared.ts +0 -147
- package/src/textarea.ts +0 -959
package/src/input.ts
DELETED
|
@@ -1,799 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
sendNativeComponentMessage,
|
|
3
|
-
registerNativeComponentHandler,
|
|
4
|
-
} from "./nativecomponent.js";
|
|
5
|
-
import { ensureComponentId, NativeComponentUpdateState } from "./component.js";
|
|
6
|
-
import { measureElement } from "./dom.js";
|
|
7
|
-
import { isHarmony, isDesktop, isMacOS, isAndroid, isIOS } from "./platform.js";
|
|
8
|
-
import {
|
|
9
|
-
getPropOrAttr as getSharedPropOrAttr,
|
|
10
|
-
getBoolAttr as getSharedBoolAttr,
|
|
11
|
-
getNumAttr as getSharedNumAttr,
|
|
12
|
-
shouldRefreshForDataAttribute as shouldRefreshSharedDataAttribute,
|
|
13
|
-
collectDataset as collectSharedDataset,
|
|
14
|
-
} from "./text_component_shared.js";
|
|
15
|
-
|
|
16
|
-
const HARMONY_PROPS_PREFIX = "data:application/json,";
|
|
17
|
-
|
|
18
|
-
// Type definitions
|
|
19
|
-
export interface LxInputEventDetail {
|
|
20
|
-
value?: string;
|
|
21
|
-
cursor?: number;
|
|
22
|
-
keyCode?: number;
|
|
23
|
-
height?: number;
|
|
24
|
-
duration?: number;
|
|
25
|
-
encryptedValue?: string;
|
|
26
|
-
encryptError?: string;
|
|
27
|
-
pass?: boolean;
|
|
28
|
-
timeout?: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface LxInputEvent extends CustomEvent<LxInputEventDetail> {
|
|
32
|
-
detail: LxInputEventDetail;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type LxInputAttributes = {
|
|
36
|
-
id?: string;
|
|
37
|
-
value?: string;
|
|
38
|
-
type?: 'text' | 'number' | 'password' | 'digit';
|
|
39
|
-
password?: boolean | string;
|
|
40
|
-
placeholder?: string;
|
|
41
|
-
'placeholder-style'?: string;
|
|
42
|
-
'placeholder-class'?: string;
|
|
43
|
-
maxlength?: number | string;
|
|
44
|
-
'cursor-spacing'?: number | string;
|
|
45
|
-
'auto-focus'?: boolean | string;
|
|
46
|
-
disabled?: boolean | string;
|
|
47
|
-
focus?: boolean | string;
|
|
48
|
-
'confirm-type'?: 'send' | 'search' | 'next' | 'go' | 'done';
|
|
49
|
-
'always-embed'?: boolean | string;
|
|
50
|
-
'confirm-hold'?: boolean | string;
|
|
51
|
-
cursor?: number | string;
|
|
52
|
-
'cursor-color'?: string;
|
|
53
|
-
'selection-start'?: number | string;
|
|
54
|
-
'selection-end'?: number | string;
|
|
55
|
-
'adjust-position'?: boolean | string;
|
|
56
|
-
'hold-keyboard'?: boolean | string;
|
|
57
|
-
className?: string;
|
|
58
|
-
style?: any;
|
|
59
|
-
ref?: any;
|
|
60
|
-
onInput?: (e: LxInputEvent) => void;
|
|
61
|
-
onChange?: (e: LxInputEvent) => void;
|
|
62
|
-
onFocus?: (e: LxInputEvent) => void;
|
|
63
|
-
onBlur?: (e: LxInputEvent) => void;
|
|
64
|
-
onConfirm?: (e: LxInputEvent) => void;
|
|
65
|
-
onKeyboardheightchange?: (e: LxInputEvent) => void;
|
|
66
|
-
onNicknamereview?: (e: LxInputEvent) => void;
|
|
67
|
-
pageBindings?: Record<string, string>;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
declare global {
|
|
71
|
-
namespace JSX {
|
|
72
|
-
interface IntrinsicElements {
|
|
73
|
-
"lx-input": LxInputAttributes;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Component implementation
|
|
79
|
-
export class LxInputElement extends HTMLElement {
|
|
80
|
-
static get observedAttributes() {
|
|
81
|
-
return [
|
|
82
|
-
"id", "value", "type", "placeholder", "placeholder-style",
|
|
83
|
-
"placeholder-class", "password", "maxlength", "cursor-spacing",
|
|
84
|
-
"auto-focus", "disabled", "focus", "confirm-type", "always-embed",
|
|
85
|
-
"confirm-hold", "cursor", "cursor-color", "selection-start",
|
|
86
|
-
"selection-end", "adjust-position", "hold-keyboard"
|
|
87
|
-
];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
private componentId: string | null = null;
|
|
91
|
-
private mounted = false;
|
|
92
|
-
private unregister?: () => void;
|
|
93
|
-
private _handlers: Record<string, EventListenerOrEventListenerObject> = {};
|
|
94
|
-
private rawHandlers: Record<string, EventListenerOrEventListenerObject> = {};
|
|
95
|
-
private harmonyEmbed?: HTMLEmbedElement;
|
|
96
|
-
private lastHarmonyProps?: string;
|
|
97
|
-
private pendingHarmonyProps?: Record<string, unknown>;
|
|
98
|
-
private pendingHarmonyRetryTimer: number | null = null;
|
|
99
|
-
private pendingHarmonyRetryCount = 0;
|
|
100
|
-
private attrObserver?: MutationObserver;
|
|
101
|
-
private shadow?: ShadowRoot;
|
|
102
|
-
private innerInput?: HTMLInputElement;
|
|
103
|
-
private readonly updateState = new NativeComponentUpdateState();
|
|
104
|
-
private resizeObserver?: ResizeObserver;
|
|
105
|
-
private pendingLayoutFrame: number | null = null;
|
|
106
|
-
private focusLayoutSyncTimer: number | null = null;
|
|
107
|
-
private readonly layoutEventListener: EventListenerObject = {
|
|
108
|
-
handleEvent: () => this.schedulePositionSync()
|
|
109
|
-
};
|
|
110
|
-
private _pageBindings: Record<string, string> = {};
|
|
111
|
-
|
|
112
|
-
set pageBindings(bindings: Record<string, string>) {
|
|
113
|
-
this._pageBindings = bindings ?? {};
|
|
114
|
-
if (this.isConnected) {
|
|
115
|
-
this.mountInput();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
get pageBindings(): Record<string, string> {
|
|
120
|
-
return this._pageBindings;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private upgradeProperty(propName: string): void {
|
|
124
|
-
const self = this as unknown as Record<string, unknown>;
|
|
125
|
-
if (!Object.prototype.hasOwnProperty.call(self, propName)) return;
|
|
126
|
-
const value = self[propName];
|
|
127
|
-
delete self[propName];
|
|
128
|
-
self[propName] = value;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private registerNativeHandler(): void {
|
|
132
|
-
if (!this.componentId) return;
|
|
133
|
-
if (this.unregister) {
|
|
134
|
-
this.unregister();
|
|
135
|
-
this.unregister = undefined;
|
|
136
|
-
}
|
|
137
|
-
this.unregister = registerNativeComponentHandler(this.componentId, (message) => {
|
|
138
|
-
if (typeof message.event === "string") {
|
|
139
|
-
const detail = (message.detail ?? {}) as LxInputEventDetail;
|
|
140
|
-
if (message.event === "keyboardheightchange") {
|
|
141
|
-
this.resyncHarmonyLayoutAfterFocus();
|
|
142
|
-
} else if (message.event === "focus") {
|
|
143
|
-
this.startFocusLayoutSync();
|
|
144
|
-
this.resyncHarmonyLayoutAfterFocus();
|
|
145
|
-
} else if (message.event === "blur") {
|
|
146
|
-
this.stopFocusLayoutSync();
|
|
147
|
-
}
|
|
148
|
-
this.dispatchEvent(new CustomEvent(message.event, { detail, bubbles: true }));
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
private unmountNativeComponentById(componentId: string): void {
|
|
154
|
-
if (!componentId || isHarmony() || isDesktop() || isAndroid()) return;
|
|
155
|
-
sendNativeComponentMessage({
|
|
156
|
-
action: "component.unmount",
|
|
157
|
-
id: componentId
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private teardownHarmonyEmbed(): void {
|
|
162
|
-
if (this.harmonyEmbed && this.contains(this.harmonyEmbed)) {
|
|
163
|
-
this.removeChild(this.harmonyEmbed);
|
|
164
|
-
}
|
|
165
|
-
this.harmonyEmbed = undefined;
|
|
166
|
-
this.lastHarmonyProps = undefined;
|
|
167
|
-
if (this.pendingHarmonyRetryTimer !== null) {
|
|
168
|
-
clearTimeout(this.pendingHarmonyRetryTimer);
|
|
169
|
-
this.pendingHarmonyRetryTimer = null;
|
|
170
|
-
}
|
|
171
|
-
this.pendingHarmonyProps = undefined;
|
|
172
|
-
this.pendingHarmonyRetryCount = 0;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
connectedCallback() {
|
|
176
|
-
this.upgradeProperty("pageBindings");
|
|
177
|
-
this.upgradeProperty("oninput");
|
|
178
|
-
this.upgradeProperty("onchange");
|
|
179
|
-
this.upgradeProperty("onfocus");
|
|
180
|
-
this.upgradeProperty("onblur");
|
|
181
|
-
this.upgradeProperty("onconfirm");
|
|
182
|
-
this.upgradeProperty("onkeyboardheightchange");
|
|
183
|
-
this.upgradeProperty("onnicknamereview");
|
|
184
|
-
|
|
185
|
-
this.componentId = ensureComponentId(this, "lx-input", this.componentId);
|
|
186
|
-
if (!this.componentId) return;
|
|
187
|
-
this.registerNativeHandler();
|
|
188
|
-
|
|
189
|
-
this.startAttrObserver();
|
|
190
|
-
this.startTracking();
|
|
191
|
-
this.mountInput();
|
|
192
|
-
queueMicrotask(() => {
|
|
193
|
-
if (!this.isConnected) return;
|
|
194
|
-
this.mountInput();
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
disconnectedCallback() {
|
|
199
|
-
if (this.shadow) {
|
|
200
|
-
this.innerInput = undefined;
|
|
201
|
-
this.shadow = undefined;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (this.componentId) {
|
|
205
|
-
this.unmountNativeComponentById(this.componentId);
|
|
206
|
-
}
|
|
207
|
-
if (this.unregister) {
|
|
208
|
-
this.unregister();
|
|
209
|
-
this.unregister = undefined;
|
|
210
|
-
}
|
|
211
|
-
this.teardownHarmonyEmbed();
|
|
212
|
-
this.stopFocusLayoutSync();
|
|
213
|
-
this.stopTracking();
|
|
214
|
-
this.mounted = false;
|
|
215
|
-
this.updateState.reset();
|
|
216
|
-
this.stopAttrObserver();
|
|
217
|
-
Object.keys(this._handlers).forEach(name => {
|
|
218
|
-
this.removeEventListener(name, this._handlers[name]);
|
|
219
|
-
});
|
|
220
|
-
this._handlers = {};
|
|
221
|
-
this.rawHandlers = {};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
attributeChangedCallback(name: string) {
|
|
225
|
-
if (name === "id") {
|
|
226
|
-
const prev = this.componentId;
|
|
227
|
-
this.componentId = ensureComponentId(this, "lx-input", this.componentId);
|
|
228
|
-
if (prev && this.componentId !== prev) {
|
|
229
|
-
this.mounted = false;
|
|
230
|
-
this.updateState.reset();
|
|
231
|
-
if (this.isConnected) {
|
|
232
|
-
this.unmountNativeComponentById(prev);
|
|
233
|
-
this.teardownHarmonyEmbed();
|
|
234
|
-
this.registerNativeHandler();
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if (!this.isConnected) return;
|
|
239
|
-
this.mountInput();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
private getAttr(name: string, fallback = "") {
|
|
243
|
-
const raw = this.getPropOrAttr(name);
|
|
244
|
-
if (raw === undefined || raw === null) return fallback;
|
|
245
|
-
return String(raw);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private getBoolAttr(name: string): boolean {
|
|
249
|
-
return getSharedBoolAttr(this, name);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private getNumAttr(name: string): number | undefined {
|
|
253
|
-
return getSharedNumAttr(this, name);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private getPropOrAttr(name: string): unknown {
|
|
257
|
-
return getSharedPropOrAttr(this, name);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
private shouldTrackNativeLayout(): boolean {
|
|
261
|
-
return isIOS() || isHarmony();
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
private startTracking(): void {
|
|
265
|
-
if (!this.shouldTrackNativeLayout()) return;
|
|
266
|
-
window.addEventListener("resize", this.layoutEventListener);
|
|
267
|
-
if (isHarmony()) {
|
|
268
|
-
// Harmony native embed coordinates need continuous sync while page scrolls.
|
|
269
|
-
window.addEventListener("scroll", this.layoutEventListener, { capture: true, passive: true });
|
|
270
|
-
} else if (isIOS()) {
|
|
271
|
-
// Match video behavior on iOS: track top-level page scrolling only.
|
|
272
|
-
window.addEventListener("scroll", this.layoutEventListener, { passive: true });
|
|
273
|
-
}
|
|
274
|
-
window.visualViewport?.addEventListener("resize", this.layoutEventListener);
|
|
275
|
-
window.visualViewport?.addEventListener("scroll", this.layoutEventListener);
|
|
276
|
-
this.startSizeObserver();
|
|
277
|
-
this.schedulePositionSync();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
private stopTracking(): void {
|
|
281
|
-
window.removeEventListener("resize", this.layoutEventListener);
|
|
282
|
-
window.removeEventListener("scroll", this.layoutEventListener, true);
|
|
283
|
-
window.removeEventListener("scroll", this.layoutEventListener);
|
|
284
|
-
window.visualViewport?.removeEventListener("resize", this.layoutEventListener);
|
|
285
|
-
window.visualViewport?.removeEventListener("scroll", this.layoutEventListener);
|
|
286
|
-
this.stopSizeObserver();
|
|
287
|
-
if (this.pendingLayoutFrame !== null) {
|
|
288
|
-
cancelAnimationFrame(this.pendingLayoutFrame);
|
|
289
|
-
this.pendingLayoutFrame = null;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
private startSizeObserver(): void {
|
|
294
|
-
if (typeof ResizeObserver === "undefined" || this.resizeObserver) return;
|
|
295
|
-
this.resizeObserver = new ResizeObserver((entries) => {
|
|
296
|
-
for (const entry of entries) {
|
|
297
|
-
if (entry.target !== this) continue;
|
|
298
|
-
this.schedulePositionSync();
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
this.resizeObserver.observe(this);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
private stopSizeObserver(): void {
|
|
306
|
-
if (!this.resizeObserver) return;
|
|
307
|
-
this.resizeObserver.disconnect();
|
|
308
|
-
this.resizeObserver = undefined;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
private schedulePositionSync(): void {
|
|
312
|
-
if (!this.shouldTrackNativeLayout() || !this.isConnected) return;
|
|
313
|
-
if (this.pendingLayoutFrame !== null) return;
|
|
314
|
-
this.pendingLayoutFrame = requestAnimationFrame(() => {
|
|
315
|
-
this.pendingLayoutFrame = null;
|
|
316
|
-
if (!this.isConnected) return;
|
|
317
|
-
this.mountInput();
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private collectProps() {
|
|
322
|
-
const autoFocus = this.getBoolAttr("auto-focus");
|
|
323
|
-
const password = this.getBoolAttr("password");
|
|
324
|
-
const value = this.getPropOrAttr("value");
|
|
325
|
-
const props: Record<string, any> = {
|
|
326
|
-
value: value === undefined || value === null ? undefined : String(value),
|
|
327
|
-
type: password ? "password" : this.getAttr("type", "text"),
|
|
328
|
-
password,
|
|
329
|
-
placeholder: this.getAttr("placeholder"),
|
|
330
|
-
placeholderStyle: this.getAttr("placeholder-style"),
|
|
331
|
-
// Use string values so "false" survives all bridge layers (some paths may drop falsy booleans).
|
|
332
|
-
focus: (this.getBoolAttr("focus") || autoFocus) ? "true" : "false",
|
|
333
|
-
autoFocus,
|
|
334
|
-
disabled: this.getBoolAttr("disabled"),
|
|
335
|
-
confirmType: this.getAttr("confirm-type", "done"),
|
|
336
|
-
alwaysEmbed: this.getBoolAttr("always-embed"),
|
|
337
|
-
confirmHold: this.getBoolAttr("confirm-hold"),
|
|
338
|
-
adjustPosition: this.getAttribute("adjust-position") !== "false",
|
|
339
|
-
holdKeyboard: this.getBoolAttr("hold-keyboard"),
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
const textColor = this.getHostTextColor();
|
|
343
|
-
if (textColor) props.textColor = textColor;
|
|
344
|
-
|
|
345
|
-
const placeholderClass = this.getAttr("placeholder-class");
|
|
346
|
-
if (placeholderClass) props.placeholderClass = placeholderClass;
|
|
347
|
-
|
|
348
|
-
const maxlength = this.getNumAttr("maxlength");
|
|
349
|
-
if (maxlength !== undefined) props.maxlength = maxlength;
|
|
350
|
-
|
|
351
|
-
const cursorSpacing = this.getNumAttr("cursor-spacing");
|
|
352
|
-
if (cursorSpacing !== undefined) props.cursorSpacing = cursorSpacing;
|
|
353
|
-
|
|
354
|
-
const cursor = this.getNumAttr("cursor");
|
|
355
|
-
if (cursor !== undefined) props.cursor = cursor;
|
|
356
|
-
|
|
357
|
-
const cursorColor = this.getAttr("cursor-color");
|
|
358
|
-
if (cursorColor) props.cursorColor = cursorColor;
|
|
359
|
-
|
|
360
|
-
const selStart = this.getNumAttr("selection-start");
|
|
361
|
-
if (selStart !== undefined) props.selectionStart = selStart;
|
|
362
|
-
|
|
363
|
-
const selEnd = this.getNumAttr("selection-end");
|
|
364
|
-
if (selEnd !== undefined) props.selectionEnd = selEnd;
|
|
365
|
-
|
|
366
|
-
return props;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
private getHostTextColor(): string | undefined {
|
|
370
|
-
const color = getComputedStyle(this).color?.trim();
|
|
371
|
-
if (!color) return undefined;
|
|
372
|
-
if (color === "transparent" || color === "rgba(0, 0, 0, 0)") return undefined;
|
|
373
|
-
return color;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
private resyncHarmonyLayoutAfterFocus(): void {
|
|
377
|
-
if (!isHarmony() || !this.isConnected) return;
|
|
378
|
-
this.mountInput();
|
|
379
|
-
setTimeout(() => {
|
|
380
|
-
if (!this.isConnected) return;
|
|
381
|
-
this.mountInput();
|
|
382
|
-
}, 32);
|
|
383
|
-
setTimeout(() => {
|
|
384
|
-
if (!this.isConnected) return;
|
|
385
|
-
this.mountInput();
|
|
386
|
-
}, 96);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
private startFocusLayoutSync(): void {
|
|
390
|
-
if (!isHarmony()) return;
|
|
391
|
-
if (this.focusLayoutSyncTimer !== null) return;
|
|
392
|
-
this.focusLayoutSyncTimer = window.setInterval(() => {
|
|
393
|
-
if (!this.isConnected) {
|
|
394
|
-
this.stopFocusLayoutSync();
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
this.mountInput();
|
|
398
|
-
}, 80);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
private stopFocusLayoutSync(): void {
|
|
402
|
-
if (this.focusLayoutSyncTimer === null) return;
|
|
403
|
-
clearInterval(this.focusLayoutSyncTimer);
|
|
404
|
-
this.focusLayoutSyncTimer = null;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
private shouldRefreshForAttribute(name: string): boolean {
|
|
408
|
-
return shouldRefreshSharedDataAttribute(name);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
private startAttrObserver(): void {
|
|
412
|
-
if (typeof MutationObserver === "undefined" || this.attrObserver) return;
|
|
413
|
-
this.attrObserver = new MutationObserver((mutations) => {
|
|
414
|
-
for (const mutation of mutations) {
|
|
415
|
-
const attrName = mutation.attributeName;
|
|
416
|
-
if (!attrName || !this.shouldRefreshForAttribute(attrName)) continue;
|
|
417
|
-
this.mountInput();
|
|
418
|
-
break;
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
this.attrObserver.observe(this, { attributes: true });
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
private stopAttrObserver(): void {
|
|
425
|
-
if (!this.attrObserver) return;
|
|
426
|
-
this.attrObserver.disconnect();
|
|
427
|
-
this.attrObserver = undefined;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
private collectDataset(): Record<string, string> {
|
|
431
|
-
return collectSharedDataset(this);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
private getInputType(): string {
|
|
435
|
-
if (this.getBoolAttr("password")) return "password";
|
|
436
|
-
const type = this.getAttr("type", "text");
|
|
437
|
-
switch (type) {
|
|
438
|
-
case "digit": return "tel";
|
|
439
|
-
case "number": return "number";
|
|
440
|
-
case "password": return "password";
|
|
441
|
-
case "safe-password": return "password";
|
|
442
|
-
default: return "text";
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
private mountDesktopInput() {
|
|
447
|
-
if (!this.shadow) {
|
|
448
|
-
this.shadow = this.attachShadow({ mode: "open" });
|
|
449
|
-
const style = document.createElement("style");
|
|
450
|
-
if (isAndroid() || isMacOS()) {
|
|
451
|
-
// Native app context: page provides visual container; keep the input bare
|
|
452
|
-
style.textContent = `
|
|
453
|
-
:host { display: block; width: 100%; height: 100%; }
|
|
454
|
-
input {
|
|
455
|
-
display: block; width: 100%; height: 100%;
|
|
456
|
-
box-sizing: border-box; padding: 0; margin: 0;
|
|
457
|
-
background: transparent; border: none; outline: none;
|
|
458
|
-
color: inherit; font: inherit;
|
|
459
|
-
}
|
|
460
|
-
`;
|
|
461
|
-
} else {
|
|
462
|
-
style.textContent = `
|
|
463
|
-
:host {
|
|
464
|
-
display: block;
|
|
465
|
-
width: 100%;
|
|
466
|
-
}
|
|
467
|
-
input {
|
|
468
|
-
display: block;
|
|
469
|
-
width: 100%;
|
|
470
|
-
box-sizing: border-box;
|
|
471
|
-
padding: 8px 12px;
|
|
472
|
-
border: 1px solid #d1d5db;
|
|
473
|
-
border-radius: 6px;
|
|
474
|
-
font-size: 14px;
|
|
475
|
-
line-height: 1.5;
|
|
476
|
-
color: #111827;
|
|
477
|
-
background: #fff;
|
|
478
|
-
outline: none;
|
|
479
|
-
transition: border-color 0.2s;
|
|
480
|
-
}
|
|
481
|
-
input:focus {
|
|
482
|
-
border-color: #3b82f6;
|
|
483
|
-
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
|
|
484
|
-
}
|
|
485
|
-
input:disabled {
|
|
486
|
-
background: #f3f4f6;
|
|
487
|
-
color: #9ca3af;
|
|
488
|
-
cursor: not-allowed;
|
|
489
|
-
}
|
|
490
|
-
input::placeholder {
|
|
491
|
-
color: #9ca3af;
|
|
492
|
-
}
|
|
493
|
-
`;
|
|
494
|
-
}
|
|
495
|
-
this.shadow.appendChild(style);
|
|
496
|
-
|
|
497
|
-
const input = document.createElement("input");
|
|
498
|
-
this.innerInput = input;
|
|
499
|
-
this.shadow.appendChild(input);
|
|
500
|
-
|
|
501
|
-
// Stop native composed events from leaking through shadow boundary
|
|
502
|
-
// to avoid double-firing (native retarget + our CustomEvent).
|
|
503
|
-
input.addEventListener("input", (e) => {
|
|
504
|
-
e.stopPropagation();
|
|
505
|
-
const detail: LxInputEventDetail = { value: input.value };
|
|
506
|
-
this.dispatchEvent(new CustomEvent("input", { detail, bubbles: true }));
|
|
507
|
-
});
|
|
508
|
-
input.addEventListener("change", (e) => {
|
|
509
|
-
e.stopPropagation();
|
|
510
|
-
const detail: LxInputEventDetail = { value: input.value };
|
|
511
|
-
this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true }));
|
|
512
|
-
});
|
|
513
|
-
input.addEventListener("focus", (e) => {
|
|
514
|
-
e.stopPropagation();
|
|
515
|
-
const detail: LxInputEventDetail = { value: input.value };
|
|
516
|
-
this.dispatchEvent(new CustomEvent("focus", { detail, bubbles: true }));
|
|
517
|
-
});
|
|
518
|
-
input.addEventListener("blur", (e) => {
|
|
519
|
-
e.stopPropagation();
|
|
520
|
-
const detail: LxInputEventDetail = { value: input.value };
|
|
521
|
-
this.dispatchEvent(new CustomEvent("blur", { detail, bubbles: true }));
|
|
522
|
-
});
|
|
523
|
-
input.addEventListener("keydown", (e) => {
|
|
524
|
-
if (e.key === "Enter") {
|
|
525
|
-
const detail: LxInputEventDetail = { value: input.value };
|
|
526
|
-
this.dispatchEvent(new CustomEvent("confirm", { detail, bubbles: true }));
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Sync attributes to inner input
|
|
532
|
-
const input = this.innerInput!;
|
|
533
|
-
const props = this.collectProps();
|
|
534
|
-
input.type = this.getInputType();
|
|
535
|
-
input.placeholder = props.placeholder || "";
|
|
536
|
-
input.disabled = props.disabled;
|
|
537
|
-
if (props.maxlength !== undefined) {
|
|
538
|
-
input.maxLength = props.maxlength;
|
|
539
|
-
} else {
|
|
540
|
-
input.removeAttribute("maxlength");
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// Only sync value if it differs (avoid cursor jump)
|
|
544
|
-
if (props.value !== undefined && input.value !== props.value) {
|
|
545
|
-
input.value = props.value;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Apply selection range
|
|
549
|
-
if (
|
|
550
|
-
typeof props.selectionStart === "number" &&
|
|
551
|
-
typeof props.selectionEnd === "number" &&
|
|
552
|
-
props.selectionStart >= 0 &&
|
|
553
|
-
props.selectionEnd >= props.selectionStart
|
|
554
|
-
) {
|
|
555
|
-
input.setSelectionRange(props.selectionStart, props.selectionEnd);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Apply placeholder style (replace on every update so dynamic changes work)
|
|
559
|
-
const placeholderStyle = props.placeholderStyle;
|
|
560
|
-
const styleEl = this.shadow!.querySelector("style")!;
|
|
561
|
-
const marker = "/* placeholder-style */";
|
|
562
|
-
const markerIdx = styleEl.textContent!.indexOf(marker);
|
|
563
|
-
if (placeholderStyle) {
|
|
564
|
-
const rule = `\n${marker}\ninput::placeholder { ${placeholderStyle} }`;
|
|
565
|
-
if (markerIdx >= 0) {
|
|
566
|
-
styleEl.textContent = styleEl.textContent!.substring(0, markerIdx) + rule;
|
|
567
|
-
} else {
|
|
568
|
-
styleEl.textContent += rule;
|
|
569
|
-
}
|
|
570
|
-
} else if (markerIdx >= 0) {
|
|
571
|
-
styleEl.textContent = styleEl.textContent!.substring(0, markerIdx);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
if (isAndroid() || isMacOS()) {
|
|
575
|
-
const confirmType = this.getAttr("confirm-type", "done");
|
|
576
|
-
input.setAttribute("enterkeyhint", confirmType);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
if (props.focus === "true" && document.activeElement !== input) {
|
|
580
|
-
input.focus();
|
|
581
|
-
} else if (props.focus === "false" && document.activeElement === input) {
|
|
582
|
-
input.blur();
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
private mountInput() {
|
|
587
|
-
if (!this.componentId) return;
|
|
588
|
-
this.ensureHostLayout();
|
|
589
|
-
|
|
590
|
-
const props = this.collectProps();
|
|
591
|
-
const dataset = this.collectDataset();
|
|
592
|
-
props.dataset = dataset;
|
|
593
|
-
props.datasetJson = JSON.stringify(dataset);
|
|
594
|
-
props.pageFuncBindings = this._pageBindings;
|
|
595
|
-
props.pageFuncBindingsJson = JSON.stringify(this._pageBindings);
|
|
596
|
-
const { rect, cornerRadius } = measureElement(this);
|
|
597
|
-
props.__layoutX = rect.x;
|
|
598
|
-
props.__layoutY = rect.y;
|
|
599
|
-
props.__layoutWidth = rect.width;
|
|
600
|
-
props.__layoutHeight = rect.height;
|
|
601
|
-
|
|
602
|
-
if (isHarmony()) {
|
|
603
|
-
this.ensureHarmonyEmbed(rect, props, cornerRadius);
|
|
604
|
-
this.mounted = true;
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
if (isDesktop() || isAndroid()) {
|
|
609
|
-
this.mountDesktopInput();
|
|
610
|
-
this.mounted = true;
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const zIndex = 9999;
|
|
615
|
-
const decision = this.updateState.decide(rect, props, zIndex, !this.mounted);
|
|
616
|
-
if (!decision.shouldSend) {
|
|
617
|
-
this.mounted = true;
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const payload: any = {
|
|
622
|
-
action: this.mounted ? "component.update" : "component.mount",
|
|
623
|
-
id: this.componentId,
|
|
624
|
-
type: "input.native",
|
|
625
|
-
rect,
|
|
626
|
-
zIndex
|
|
627
|
-
};
|
|
628
|
-
if (!this.mounted || decision.propsChanged) {
|
|
629
|
-
payload.props = props;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
if (cornerRadius !== undefined) {
|
|
633
|
-
payload.cornerRadius = cornerRadius;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
sendNativeComponentMessage(payload);
|
|
637
|
-
this.mounted = true;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Force a full native sync when host frameworks mutate properties without
|
|
641
|
-
// triggering observed attribute callbacks on custom elements.
|
|
642
|
-
syncNativeProps(): void {
|
|
643
|
-
if (!this.isConnected) return;
|
|
644
|
-
this.mountInput();
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
private ensureHostLayout(): void {
|
|
648
|
-
const computed = getComputedStyle(this);
|
|
649
|
-
if (computed.display === "inline") {
|
|
650
|
-
this.style.display = "block";
|
|
651
|
-
}
|
|
652
|
-
const height = Number.parseFloat(computed.height);
|
|
653
|
-
if (!Number.isFinite(height) || height <= 1) {
|
|
654
|
-
this.style.height = "40px";
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Event handler getter/setter for 'input' event
|
|
659
|
-
set oninput(cb: EventListener | null) {
|
|
660
|
-
const raw = cb as unknown;
|
|
661
|
-
const current = this._handlers['input'];
|
|
662
|
-
if (current) this.removeEventListener('input', current);
|
|
663
|
-
|
|
664
|
-
if (
|
|
665
|
-
typeof raw === "function" ||
|
|
666
|
-
(!!raw && typeof raw === "object" && typeof (raw as EventListenerObject).handleEvent === "function")
|
|
667
|
-
) {
|
|
668
|
-
const listener =
|
|
669
|
-
typeof raw === "function"
|
|
670
|
-
? ({ handleEvent: raw } as EventListenerObject)
|
|
671
|
-
: (raw as EventListenerOrEventListenerObject);
|
|
672
|
-
this._handlers['input'] = listener;
|
|
673
|
-
this.rawHandlers['input'] = raw as EventListenerOrEventListenerObject;
|
|
674
|
-
this.addEventListener('input', listener);
|
|
675
|
-
} else {
|
|
676
|
-
delete this._handlers['input'];
|
|
677
|
-
delete this.rawHandlers['input'];
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
get oninput() { return (this.rawHandlers['input'] as EventListener) || null; }
|
|
681
|
-
|
|
682
|
-
private ensureHarmonyEmbed(
|
|
683
|
-
rect: { width: number; height: number },
|
|
684
|
-
props: Record<string, unknown>,
|
|
685
|
-
cornerRadius?: number
|
|
686
|
-
) {
|
|
687
|
-
if (!this.harmonyEmbed) {
|
|
688
|
-
const embed = document.createElement("embed");
|
|
689
|
-
embed.setAttribute("type", "native/input");
|
|
690
|
-
embed.setAttribute("width", `${rect.width}`);
|
|
691
|
-
embed.setAttribute("height", `${rect.height}`);
|
|
692
|
-
embed.setAttribute("id", this.componentId!);
|
|
693
|
-
embed.setAttribute("data-lx-component-id", this.componentId!);
|
|
694
|
-
embed.style.display = "block";
|
|
695
|
-
embed.style.width = "100%";
|
|
696
|
-
embed.style.height = "100%";
|
|
697
|
-
embed.style.border = "none";
|
|
698
|
-
if (cornerRadius !== undefined) {
|
|
699
|
-
embed.style.borderRadius = `${cornerRadius}px`;
|
|
700
|
-
embed.style.overflow = "hidden";
|
|
701
|
-
}
|
|
702
|
-
const encodedProps = this.encodeHarmonyProps(props);
|
|
703
|
-
embed.setAttribute("src", encodedProps);
|
|
704
|
-
this.lastHarmonyProps = encodedProps;
|
|
705
|
-
this.appendChild(embed);
|
|
706
|
-
this.harmonyEmbed = embed;
|
|
707
|
-
this.pendingHarmonyRetryCount = 0;
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
const encodedProps = this.encodeHarmonyProps(props);
|
|
712
|
-
if (encodedProps !== this.lastHarmonyProps) {
|
|
713
|
-
const pushed = this.pushHarmonyPropsUpdate(props);
|
|
714
|
-
if (pushed) {
|
|
715
|
-
this.lastHarmonyProps = encodedProps;
|
|
716
|
-
this.pendingHarmonyProps = undefined;
|
|
717
|
-
this.pendingHarmonyRetryCount = 0;
|
|
718
|
-
} else {
|
|
719
|
-
// Fallback to src update path when proxy bridge is unavailable.
|
|
720
|
-
this.harmonyEmbed.setAttribute("src", encodedProps);
|
|
721
|
-
this.lastHarmonyProps = encodedProps;
|
|
722
|
-
this.scheduleHarmonyPropsRetry(props);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
if (cornerRadius !== undefined && this.harmonyEmbed.style.borderRadius !== `${cornerRadius}px`) {
|
|
727
|
-
this.harmonyEmbed.style.borderRadius = `${cornerRadius}px`;
|
|
728
|
-
this.harmonyEmbed.style.overflow = "hidden";
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
private encodeHarmonyProps(props: Record<string, unknown>): string {
|
|
733
|
-
try {
|
|
734
|
-
const payload = { componentId: this.componentId, ...props };
|
|
735
|
-
return `${HARMONY_PROPS_PREFIX}${encodeURIComponent(JSON.stringify(payload))}`;
|
|
736
|
-
} catch (_e) {
|
|
737
|
-
return `${HARMONY_PROPS_PREFIX}%7B%7D`;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
private pushHarmonyPropsUpdate(props: Record<string, unknown>): boolean {
|
|
742
|
-
if (!this.componentId || !isHarmony()) return false;
|
|
743
|
-
try {
|
|
744
|
-
const proxy = (window as unknown as Record<string, unknown>).LingXiaProxy as
|
|
745
|
-
| { nativeComponentUpdate?: ((componentId: string, payload: string) => void) | ((payload: string) => void) }
|
|
746
|
-
| undefined;
|
|
747
|
-
const updateFn = proxy?.nativeComponentUpdate;
|
|
748
|
-
if (typeof updateFn !== "function") {
|
|
749
|
-
return false;
|
|
750
|
-
}
|
|
751
|
-
const payload = JSON.stringify({ componentId: this.componentId, ...props });
|
|
752
|
-
try {
|
|
753
|
-
(updateFn as (payload: string) => void).call(proxy, payload);
|
|
754
|
-
return true;
|
|
755
|
-
} catch {
|
|
756
|
-
try {
|
|
757
|
-
(updateFn as (componentId: string, payload: string) => void).call(proxy, this.componentId, payload);
|
|
758
|
-
return true;
|
|
759
|
-
} catch {
|
|
760
|
-
return false;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
} catch {
|
|
764
|
-
return false;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
private scheduleHarmonyPropsRetry(props: Record<string, unknown>): void {
|
|
769
|
-
this.pendingHarmonyProps = { ...props };
|
|
770
|
-
if (this.pendingHarmonyRetryTimer !== null) {
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
this.pendingHarmonyRetryTimer = window.setTimeout(() => {
|
|
774
|
-
this.pendingHarmonyRetryTimer = null;
|
|
775
|
-
if (!this.isConnected || !this.pendingHarmonyProps) {
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
const pendingProps = this.pendingHarmonyProps;
|
|
779
|
-
const pushed = this.pushHarmonyPropsUpdate(pendingProps);
|
|
780
|
-
if (pushed) {
|
|
781
|
-
this.lastHarmonyProps = this.encodeHarmonyProps(pendingProps);
|
|
782
|
-
this.pendingHarmonyProps = undefined;
|
|
783
|
-
this.pendingHarmonyRetryCount = 0;
|
|
784
|
-
} else {
|
|
785
|
-
this.pendingHarmonyRetryCount += 1;
|
|
786
|
-
if (this.pendingHarmonyRetryCount < 20) {
|
|
787
|
-
this.scheduleHarmonyPropsRetry(pendingProps);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}, 100);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// Register the custom element
|
|
795
|
-
export function registerInputComponent() {
|
|
796
|
-
if (typeof window !== "undefined" && !customElements.get("lx-input")) {
|
|
797
|
-
customElements.define("lx-input", LxInputElement);
|
|
798
|
-
}
|
|
799
|
-
}
|