@lingxia/elements 0.8.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/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
- }