@lingxia/elements 0.9.0 → 0.11.1

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