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