@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.
- package/dist/component.d.ts +50 -0
- package/dist/component.js +139 -0
- package/dist/dom.d.ts +10 -0
- package/dist/dom.js +70 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/input.d.ts +117 -0
- package/dist/input.js +699 -0
- package/dist/native_component_wrapper_shared.d.ts +74 -0
- package/dist/native_component_wrapper_shared.js +193 -0
- package/dist/nativecomponent.d.ts +6 -0
- package/dist/nativecomponent.js +82 -0
- package/dist/navigator.d.ts +83 -0
- package/dist/navigator.js +388 -0
- package/dist/picker-web.d.ts +2 -0
- package/dist/picker-web.js +727 -0
- package/dist/picker.d.ts +77 -0
- package/dist/picker.js +342 -0
- package/dist/platform.d.ts +6 -0
- package/dist/platform.js +24 -0
- package/dist/text_component_native_attrs.d.ts +53 -0
- package/dist/text_component_native_attrs.js +111 -0
- package/dist/text_component_shared.d.ts +7 -0
- package/dist/text_component_shared.js +148 -0
- package/dist/textarea.d.ts +129 -0
- package/dist/textarea.js +841 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +1 -0
- package/dist/video.d.ts +134 -0
- package/dist/video.js +741 -0
- package/package.json +39 -0
- package/src/component.ts +164 -0
- package/src/dom.ts +83 -0
- package/src/index.ts +69 -0
- package/src/input.ts +799 -0
- package/src/native_component_wrapper_shared.ts +243 -0
- package/src/nativecomponent.ts +97 -0
- package/src/navigator.ts +533 -0
- package/src/picker-web.ts +719 -0
- package/src/picker.ts +418 -0
- package/src/platform.ts +25 -0
- package/src/text_component_native_attrs.ts +137 -0
- package/src/text_component_shared.ts +147 -0
- package/src/textarea.ts +959 -0
- package/src/types.ts +20 -0
- package/src/video.ts +855 -0
package/dist/video.js
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import { sendNativeComponentMessage, registerNativeComponentHandler, addNativeComponentLayoutInvalidationListener } from "./nativecomponent.js";
|
|
2
|
+
import { measureElement } from "./dom.js";
|
|
3
|
+
import { ensureComponentId, NativeComponentUpdateState, iOSNativeComponentHelper } from "./component.js";
|
|
4
|
+
import { isAndroid, isHarmony, isIOS } from "./platform.js";
|
|
5
|
+
const HARMONY_PROPS_PREFIX = "data:application/json,";
|
|
6
|
+
// Component implementation
|
|
7
|
+
export class LxVideoElement extends HTMLElement {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.componentId = null;
|
|
11
|
+
this.mounted = false;
|
|
12
|
+
this.updateState = new NativeComponentUpdateState();
|
|
13
|
+
this.pendingLayoutFrame = null;
|
|
14
|
+
this.boundUpdatePosition = this.updatePosition.bind(this);
|
|
15
|
+
this._handlers = {};
|
|
16
|
+
this.pendingHarmonyRetryTimer = null;
|
|
17
|
+
this.pendingHarmonyRetryCount = 0;
|
|
18
|
+
this.iOSBootstrapFrame = null;
|
|
19
|
+
this.iOSBootstrapRemaining = 0;
|
|
20
|
+
this.forceHarmonyEmbedRecreate = false;
|
|
21
|
+
this.resizeEventListener = {
|
|
22
|
+
handleEvent: () => this.boundUpdatePosition(),
|
|
23
|
+
};
|
|
24
|
+
this.iOSScrollEventListener = {
|
|
25
|
+
handleEvent: () => this.boundUpdatePosition(),
|
|
26
|
+
};
|
|
27
|
+
this.rawHandlers = {};
|
|
28
|
+
this._pageBindings = {};
|
|
29
|
+
}
|
|
30
|
+
static get observedAttributes() {
|
|
31
|
+
return [
|
|
32
|
+
"id",
|
|
33
|
+
"src",
|
|
34
|
+
"poster",
|
|
35
|
+
"object-fit",
|
|
36
|
+
"rotate",
|
|
37
|
+
"autoplay",
|
|
38
|
+
"loop",
|
|
39
|
+
"muted",
|
|
40
|
+
"controls",
|
|
41
|
+
"progress-bar",
|
|
42
|
+
"live",
|
|
43
|
+
"volume",
|
|
44
|
+
"qualities",
|
|
45
|
+
"playback-rates"
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
parseRotateValue(value) {
|
|
49
|
+
const parsed = (() => {
|
|
50
|
+
if (typeof value === "number") {
|
|
51
|
+
return Number.isInteger(value) ? value : NaN;
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === "string") {
|
|
54
|
+
const normalized = value.trim();
|
|
55
|
+
if (!/^(0|90|180|270)$/.test(normalized))
|
|
56
|
+
return NaN;
|
|
57
|
+
return Number(normalized);
|
|
58
|
+
}
|
|
59
|
+
return NaN;
|
|
60
|
+
})();
|
|
61
|
+
if (parsed === 0 || parsed === 90 || parsed === 180 || parsed === 270) {
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
parseObjectFitValue(value) {
|
|
67
|
+
if (typeof value !== "string")
|
|
68
|
+
return undefined;
|
|
69
|
+
const normalized = value.trim().toLowerCase();
|
|
70
|
+
if (normalized === "cover" ||
|
|
71
|
+
normalized === "contain" ||
|
|
72
|
+
normalized === "fill" ||
|
|
73
|
+
normalized === "fit") {
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
set pageBindings(bindings) {
|
|
79
|
+
this._pageBindings = bindings !== null && bindings !== void 0 ? bindings : {};
|
|
80
|
+
if (this.isConnected) {
|
|
81
|
+
this.mountOrUpdate(true);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
get pageBindings() {
|
|
85
|
+
return this._pageBindings;
|
|
86
|
+
}
|
|
87
|
+
dataAttrToDatasetKey(attr) {
|
|
88
|
+
const raw = attr.slice(5).trim();
|
|
89
|
+
if (!raw)
|
|
90
|
+
return "";
|
|
91
|
+
const parts = raw.split("-").filter(Boolean);
|
|
92
|
+
if (parts.length === 0)
|
|
93
|
+
return "";
|
|
94
|
+
return parts
|
|
95
|
+
.map((segment, index) => {
|
|
96
|
+
if (index === 0)
|
|
97
|
+
return segment.toLowerCase();
|
|
98
|
+
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
99
|
+
})
|
|
100
|
+
.join("");
|
|
101
|
+
}
|
|
102
|
+
collectDataset() {
|
|
103
|
+
const dataset = {};
|
|
104
|
+
const attrs = this.getAttributeNames();
|
|
105
|
+
for (const attr of attrs) {
|
|
106
|
+
if (!attr.startsWith("data-"))
|
|
107
|
+
continue;
|
|
108
|
+
const key = this.dataAttrToDatasetKey(attr);
|
|
109
|
+
if (!key)
|
|
110
|
+
continue;
|
|
111
|
+
const value = this.getAttribute(attr);
|
|
112
|
+
if (value == null)
|
|
113
|
+
continue;
|
|
114
|
+
dataset[key] = value;
|
|
115
|
+
}
|
|
116
|
+
return dataset;
|
|
117
|
+
}
|
|
118
|
+
upgradeProperty(propName) {
|
|
119
|
+
const self = this;
|
|
120
|
+
if (!Object.prototype.hasOwnProperty.call(self, propName))
|
|
121
|
+
return;
|
|
122
|
+
const value = self[propName];
|
|
123
|
+
delete self[propName];
|
|
124
|
+
self[propName] = value;
|
|
125
|
+
}
|
|
126
|
+
set rotate(value) {
|
|
127
|
+
const normalized = this.parseRotateValue(value);
|
|
128
|
+
if (normalized === undefined) {
|
|
129
|
+
this.removeAttribute("rotate");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.setAttribute("rotate", String(normalized));
|
|
133
|
+
}
|
|
134
|
+
get rotate() {
|
|
135
|
+
return this.parseRotateValue(this.getAttribute("rotate"));
|
|
136
|
+
}
|
|
137
|
+
connectedCallback() {
|
|
138
|
+
// React may set custom-element properties before upgrade; replay through setters.
|
|
139
|
+
this.upgradeProperty("pageBindings");
|
|
140
|
+
this.upgradeProperty("rotate");
|
|
141
|
+
this.upgradeProperty("onplayrequest");
|
|
142
|
+
this.upgradeProperty("onplay");
|
|
143
|
+
this.upgradeProperty("onplaying");
|
|
144
|
+
this.upgradeProperty("onpause");
|
|
145
|
+
this.upgradeProperty("onstop");
|
|
146
|
+
this.upgradeProperty("onended");
|
|
147
|
+
this.upgradeProperty("ontimeupdate");
|
|
148
|
+
this.upgradeProperty("onloadedmetadata");
|
|
149
|
+
this.upgradeProperty("onfullscreenchange");
|
|
150
|
+
this.upgradeProperty("onwaiting");
|
|
151
|
+
this.upgradeProperty("onqualitychange");
|
|
152
|
+
this.upgradeProperty("onratechange");
|
|
153
|
+
this.componentId = ensureComponentId(this, "lx-video", this.componentId);
|
|
154
|
+
if (!this.componentId) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.unregister = registerNativeComponentHandler(this.componentId, (message) => {
|
|
158
|
+
// Handle component events from native
|
|
159
|
+
if (message.event) {
|
|
160
|
+
// Normalize detail based on event type
|
|
161
|
+
let detail = message.detail || message.payload || {};
|
|
162
|
+
// Ensure common state events have empty detail if not provided
|
|
163
|
+
if (['playrequest', 'play', 'playing', 'pause', 'stop', 'ended', 'waiting'].includes(message.event)) {
|
|
164
|
+
if (Object.keys(detail).length === 0)
|
|
165
|
+
detail = {};
|
|
166
|
+
}
|
|
167
|
+
const ev = new CustomEvent(message.event, {
|
|
168
|
+
detail: detail,
|
|
169
|
+
bubbles: true,
|
|
170
|
+
cancelable: false
|
|
171
|
+
});
|
|
172
|
+
this.dispatchEvent(ev);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
this.ensurePlaceholderStyle();
|
|
176
|
+
// Setup iOS native component rendering helper (no-op on other platforms)
|
|
177
|
+
this.iOSHelper = new iOSNativeComponentHelper(this, this.componentId);
|
|
178
|
+
this.iOSHelper.setup();
|
|
179
|
+
this.startAttrObserver();
|
|
180
|
+
// iOS bootstrap:
|
|
181
|
+
// avoid hardcoded timeout; run an immediate mount plus a few frame-based refreshes.
|
|
182
|
+
if (isIOS()) {
|
|
183
|
+
this.mountOrUpdate(true);
|
|
184
|
+
this.startTracking();
|
|
185
|
+
this.scheduleIOSBootstrapFrames();
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
this.mountOrUpdate();
|
|
189
|
+
this.startTracking();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
disconnectedCallback() {
|
|
193
|
+
var _a;
|
|
194
|
+
this.updateState.reset();
|
|
195
|
+
if (this.componentId && !isHarmony()) {
|
|
196
|
+
sendNativeComponentMessage({
|
|
197
|
+
action: "component.unmount",
|
|
198
|
+
id: this.componentId
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
this.stopTracking();
|
|
202
|
+
if (this.unregister) {
|
|
203
|
+
this.unregister();
|
|
204
|
+
this.unregister = undefined;
|
|
205
|
+
}
|
|
206
|
+
if (this.harmonyEmbed && this.contains(this.harmonyEmbed)) {
|
|
207
|
+
this.removeChild(this.harmonyEmbed);
|
|
208
|
+
}
|
|
209
|
+
this.harmonyEmbed = undefined;
|
|
210
|
+
this.lastHarmonyProps = undefined;
|
|
211
|
+
// Cleanup iOS helper
|
|
212
|
+
(_a = this.iOSHelper) === null || _a === void 0 ? void 0 : _a.cleanup();
|
|
213
|
+
this.iOSHelper = undefined;
|
|
214
|
+
this.stopAttrObserver();
|
|
215
|
+
// Cleanup manually attached property handlers
|
|
216
|
+
Object.keys(this._handlers).forEach(name => {
|
|
217
|
+
this.removeEventListener(name, this._handlers[name]);
|
|
218
|
+
});
|
|
219
|
+
this._handlers = {};
|
|
220
|
+
this.rawHandlers = {};
|
|
221
|
+
}
|
|
222
|
+
attributeChangedCallback(name) {
|
|
223
|
+
if (name === "id") {
|
|
224
|
+
const prev = this.componentId;
|
|
225
|
+
this.componentId = ensureComponentId(this, "lx-video", this.componentId);
|
|
226
|
+
if (prev && this.componentId !== prev) {
|
|
227
|
+
this.mounted = false;
|
|
228
|
+
this.updateState.reset();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!this.isConnected)
|
|
232
|
+
return;
|
|
233
|
+
// Keep rotate/object-fit updates immediate across all native platforms.
|
|
234
|
+
const forcePropsUpdate = name === "rotate" || name === "object-fit" || name === "objectfit";
|
|
235
|
+
if (isHarmony() && forcePropsUpdate) {
|
|
236
|
+
// Bridge callbacks can race on Harmony; force a deterministic embed recreate for visual props.
|
|
237
|
+
this.forceHarmonyEmbedRecreate = true;
|
|
238
|
+
}
|
|
239
|
+
this.mountOrUpdate(forcePropsUpdate);
|
|
240
|
+
}
|
|
241
|
+
isEventListener(value) {
|
|
242
|
+
if (typeof value === "function")
|
|
243
|
+
return true;
|
|
244
|
+
if (!value || typeof value !== "object")
|
|
245
|
+
return false;
|
|
246
|
+
return typeof value.handleEvent === "function";
|
|
247
|
+
}
|
|
248
|
+
setEventHandler(name, value) {
|
|
249
|
+
const current = this._handlers[name];
|
|
250
|
+
if (current) {
|
|
251
|
+
this.removeEventListener(name, current);
|
|
252
|
+
}
|
|
253
|
+
if (this.isEventListener(value)) {
|
|
254
|
+
const listener = typeof value === "function"
|
|
255
|
+
? { handleEvent: value }
|
|
256
|
+
: value;
|
|
257
|
+
this._handlers[name] = listener;
|
|
258
|
+
this.rawHandlers[name] = value;
|
|
259
|
+
this.addEventListener(name, listener);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
delete this._handlers[name];
|
|
263
|
+
delete this.rawHandlers[name];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
getEventHandler(name) {
|
|
267
|
+
return (this.rawHandlers[name] || null);
|
|
268
|
+
}
|
|
269
|
+
// Standard Media Events for React Props (e.g. <lx-video onPlay={...} />)
|
|
270
|
+
set onplayrequest(cb) { this.setEventHandler('playrequest', cb); }
|
|
271
|
+
get onplayrequest() { return this.getEventHandler('playrequest'); }
|
|
272
|
+
set onplay(cb) { this.setEventHandler('play', cb); }
|
|
273
|
+
get onplay() { return this.getEventHandler('play'); }
|
|
274
|
+
set onplaying(cb) { this.setEventHandler('playing', cb); }
|
|
275
|
+
get onplaying() { return this.getEventHandler('playing'); }
|
|
276
|
+
set onpause(cb) { this.setEventHandler('pause', cb); }
|
|
277
|
+
get onpause() { return this.getEventHandler('pause'); }
|
|
278
|
+
set onstop(cb) { this.setEventHandler('stop', cb); }
|
|
279
|
+
get onstop() { return this.getEventHandler('stop'); }
|
|
280
|
+
set onended(cb) { this.setEventHandler('ended', cb); }
|
|
281
|
+
get onended() { return this.getEventHandler('ended'); }
|
|
282
|
+
set ontimeupdate(cb) { this.setEventHandler('timeupdate', cb); }
|
|
283
|
+
get ontimeupdate() { return this.getEventHandler('timeupdate'); }
|
|
284
|
+
// Note: onerror is inherited from HTMLElement with OnErrorEventHandler type.
|
|
285
|
+
// Use addEventListener('error', handler) for error events instead.
|
|
286
|
+
set onloadedmetadata(cb) { this.setEventHandler('loadedmetadata', cb); }
|
|
287
|
+
get onloadedmetadata() { return this.getEventHandler('loadedmetadata'); }
|
|
288
|
+
set onfullscreenchange(cb) { this.setEventHandler('fullscreenchange', cb); }
|
|
289
|
+
get onfullscreenchange() { return this.getEventHandler('fullscreenchange'); }
|
|
290
|
+
set onwaiting(cb) { this.setEventHandler('waiting', cb); }
|
|
291
|
+
get onwaiting() { return this.getEventHandler('waiting'); }
|
|
292
|
+
set onqualitychange(cb) { this.setEventHandler('qualitychange', cb); }
|
|
293
|
+
get onqualitychange() { return this.getEventHandler('qualitychange'); }
|
|
294
|
+
set onratechange(cb) { this.setEventHandler('ratechange', cb); }
|
|
295
|
+
get onratechange() { return this.getEventHandler('ratechange'); }
|
|
296
|
+
// Internal
|
|
297
|
+
ensurePlaceholderStyle() {
|
|
298
|
+
if (!this.style.display)
|
|
299
|
+
this.style.display = "block";
|
|
300
|
+
if (!this.style.position)
|
|
301
|
+
this.style.position = "relative"; // Needed for iOS scroll container
|
|
302
|
+
if (!this.style.backgroundColor)
|
|
303
|
+
this.style.backgroundColor = "black";
|
|
304
|
+
if (!this.style.aspectRatio)
|
|
305
|
+
this.style.aspectRatio = "16 / 9";
|
|
306
|
+
}
|
|
307
|
+
collectProps() {
|
|
308
|
+
var _a;
|
|
309
|
+
const volumeAttr = this.getAttribute("volume");
|
|
310
|
+
const volume = volumeAttr != null ? parseFloat(volumeAttr) : undefined;
|
|
311
|
+
const rawObjectFit = (_a = this.getAttribute("object-fit")) !== null && _a !== void 0 ? _a : this.getAttribute("objectfit");
|
|
312
|
+
const objectFit = this.parseObjectFitValue(rawObjectFit);
|
|
313
|
+
// JSON-encoded arrays (React wrapper sets these via JSON.stringify)
|
|
314
|
+
const qualitiesAttr = this.getAttribute("qualities");
|
|
315
|
+
let qualities;
|
|
316
|
+
if (qualitiesAttr) {
|
|
317
|
+
try {
|
|
318
|
+
const parsed = JSON.parse(qualitiesAttr);
|
|
319
|
+
if (Array.isArray(parsed)) {
|
|
320
|
+
qualities = parsed.filter((q) => q && typeof q === "object" && typeof q.label === "string");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch { }
|
|
324
|
+
}
|
|
325
|
+
// Parse playbackRates array from JSON attribute
|
|
326
|
+
const playbackRatesAttr = this.getAttribute("playback-rates");
|
|
327
|
+
let playbackRates;
|
|
328
|
+
if (playbackRatesAttr) {
|
|
329
|
+
try {
|
|
330
|
+
const parsed = JSON.parse(playbackRatesAttr);
|
|
331
|
+
if (Array.isArray(parsed)) {
|
|
332
|
+
playbackRates = parsed.filter((n) => typeof n === "number");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch { }
|
|
336
|
+
}
|
|
337
|
+
const isLive = this.hasAttribute("live");
|
|
338
|
+
// When live, default progressBar to false unless explicitly enabled.
|
|
339
|
+
const progressBarAttr = this.getAttribute("progress-bar");
|
|
340
|
+
const progressBar = isLive
|
|
341
|
+
? (progressBarAttr !== null && progressBarAttr !== "false")
|
|
342
|
+
: (progressBarAttr !== "false");
|
|
343
|
+
const rawRotate = this.getAttribute("rotate");
|
|
344
|
+
const validRotate = this.parseRotateValue(rawRotate);
|
|
345
|
+
const pageFuncBindings = this._pageBindings;
|
|
346
|
+
const dataset = this.collectDataset();
|
|
347
|
+
const clearProps = [];
|
|
348
|
+
if (rawRotate === null || validRotate === undefined) {
|
|
349
|
+
clearProps.push("rotate");
|
|
350
|
+
}
|
|
351
|
+
if (rawObjectFit == null || objectFit === undefined) {
|
|
352
|
+
clearProps.push("objectFit");
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
src: this.getAttribute("src") || undefined,
|
|
356
|
+
poster: this.getAttribute("poster") || undefined,
|
|
357
|
+
autoplay: this.hasAttribute("autoplay"),
|
|
358
|
+
loop: this.hasAttribute("loop"),
|
|
359
|
+
muted: this.hasAttribute("muted"),
|
|
360
|
+
controls: this.hasAttribute("controls"),
|
|
361
|
+
progressBar,
|
|
362
|
+
live: isLive,
|
|
363
|
+
volume: !Number.isNaN(volume !== null && volume !== void 0 ? volume : NaN) ? volume : undefined,
|
|
364
|
+
objectFit,
|
|
365
|
+
rotate: validRotate,
|
|
366
|
+
...(clearProps.length > 0 ? { __clearProps: clearProps } : {}),
|
|
367
|
+
qualities,
|
|
368
|
+
playbackRates,
|
|
369
|
+
dataset,
|
|
370
|
+
datasetJson: JSON.stringify(dataset),
|
|
371
|
+
pageFuncBindings,
|
|
372
|
+
pageFuncBindingsJson: JSON.stringify(pageFuncBindings)
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
shouldRefreshForAttribute(name) {
|
|
376
|
+
const normalized = name.trim().toLowerCase();
|
|
377
|
+
return normalized.startsWith("data-");
|
|
378
|
+
}
|
|
379
|
+
startAttrObserver() {
|
|
380
|
+
if (typeof MutationObserver === "undefined" || this.attrObserver) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
this.attrObserver = new MutationObserver((mutations) => {
|
|
384
|
+
for (const mutation of mutations) {
|
|
385
|
+
const attrName = mutation.attributeName;
|
|
386
|
+
if (!attrName || !this.shouldRefreshForAttribute(attrName)) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
this.mountOrUpdate(true);
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
this.attrObserver.observe(this, { attributes: true });
|
|
394
|
+
}
|
|
395
|
+
stopAttrObserver() {
|
|
396
|
+
if (!this.attrObserver) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
this.attrObserver.disconnect();
|
|
400
|
+
this.attrObserver = undefined;
|
|
401
|
+
}
|
|
402
|
+
measureForNative() {
|
|
403
|
+
const measured = measureElement(this);
|
|
404
|
+
if (!isIOS()) {
|
|
405
|
+
return measured;
|
|
406
|
+
}
|
|
407
|
+
const rect = this.getBoundingClientRect();
|
|
408
|
+
return {
|
|
409
|
+
rect: {
|
|
410
|
+
x: rect.left,
|
|
411
|
+
y: rect.top,
|
|
412
|
+
width: rect.width,
|
|
413
|
+
height: rect.height
|
|
414
|
+
},
|
|
415
|
+
cornerRadius: measured.cornerRadius
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
mountOrUpdate(forceUpdate = false) {
|
|
419
|
+
var _a;
|
|
420
|
+
if (isHarmony()) {
|
|
421
|
+
this.mountOrUpdateHarmony(forceUpdate);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (!this.componentId)
|
|
425
|
+
return;
|
|
426
|
+
const { rect, cornerRadius } = this.measureForNative();
|
|
427
|
+
const hasSize = rect.width > 0 && rect.height > 0;
|
|
428
|
+
if (!hasSize) {
|
|
429
|
+
// Defer and try again once layout stabilizes; only needed before first mount
|
|
430
|
+
if (!this.mounted) {
|
|
431
|
+
this.scheduleMountRetry();
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const props = this.collectProps();
|
|
436
|
+
if (cornerRadius !== undefined) {
|
|
437
|
+
props.cornerRadius = cornerRadius;
|
|
438
|
+
}
|
|
439
|
+
const zIndex = parseFloat(this.style.zIndex || "0") || 0;
|
|
440
|
+
const decision = this.updateState.decide(rect, props, zIndex, !this.mounted || forceUpdate);
|
|
441
|
+
if (!decision.shouldSend)
|
|
442
|
+
return;
|
|
443
|
+
const payload = {
|
|
444
|
+
action: this.mounted ? "component.update" : "component.mount",
|
|
445
|
+
id: this.componentId,
|
|
446
|
+
type: "video.native",
|
|
447
|
+
rect,
|
|
448
|
+
zIndex,
|
|
449
|
+
...(cornerRadius !== undefined ? { cornerRadius } : {})
|
|
450
|
+
};
|
|
451
|
+
// Enhance payload with platform-specific data (iOS scroll container rect)
|
|
452
|
+
(_a = this.iOSHelper) === null || _a === void 0 ? void 0 : _a.enhancePayload(payload);
|
|
453
|
+
// Only include props when they changed or on first mount
|
|
454
|
+
if (decision.propsChanged || !this.mounted) {
|
|
455
|
+
payload.props = props;
|
|
456
|
+
}
|
|
457
|
+
sendNativeComponentMessage(payload);
|
|
458
|
+
this.mounted = true;
|
|
459
|
+
}
|
|
460
|
+
updatePosition() {
|
|
461
|
+
var _a;
|
|
462
|
+
if (isHarmony()) {
|
|
463
|
+
this.mountOrUpdateHarmony();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (!this.mounted || !this.componentId)
|
|
467
|
+
return;
|
|
468
|
+
const { rect, cornerRadius } = this.measureForNative();
|
|
469
|
+
if (!rect.width || !rect.height)
|
|
470
|
+
return;
|
|
471
|
+
const zIndex = parseFloat(this.style.zIndex || "0") || 0;
|
|
472
|
+
const decision = this.updateState.decide(rect, null, zIndex);
|
|
473
|
+
if (!decision.shouldSend)
|
|
474
|
+
return;
|
|
475
|
+
const payload = {
|
|
476
|
+
action: "component.update",
|
|
477
|
+
id: this.componentId,
|
|
478
|
+
rect,
|
|
479
|
+
zIndex
|
|
480
|
+
};
|
|
481
|
+
// Keep iOS scroll-container metadata on update so native side can still
|
|
482
|
+
// promote fallback overlay into WKChildScrollView when it becomes available.
|
|
483
|
+
(_a = this.iOSHelper) === null || _a === void 0 ? void 0 : _a.enhancePayload(payload);
|
|
484
|
+
if (cornerRadius !== undefined) {
|
|
485
|
+
payload.cornerRadius = cornerRadius;
|
|
486
|
+
}
|
|
487
|
+
sendNativeComponentMessage(payload);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Start tracking component size changes.
|
|
491
|
+
*/
|
|
492
|
+
startTracking() {
|
|
493
|
+
window.addEventListener("resize", this.resizeEventListener);
|
|
494
|
+
if (isIOS()) {
|
|
495
|
+
window.addEventListener("scroll", this.iOSScrollEventListener, { passive: true });
|
|
496
|
+
}
|
|
497
|
+
if (isAndroid()) {
|
|
498
|
+
this.removeLayoutInvalidationListener = addNativeComponentLayoutInvalidationListener(this.resizeEventListener);
|
|
499
|
+
}
|
|
500
|
+
this.startSizeObserver();
|
|
501
|
+
this.updatePosition();
|
|
502
|
+
}
|
|
503
|
+
stopTracking() {
|
|
504
|
+
var _a;
|
|
505
|
+
window.removeEventListener("resize", this.resizeEventListener);
|
|
506
|
+
if (isIOS()) {
|
|
507
|
+
window.removeEventListener("scroll", this.iOSScrollEventListener);
|
|
508
|
+
}
|
|
509
|
+
(_a = this.removeLayoutInvalidationListener) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
510
|
+
this.removeLayoutInvalidationListener = undefined;
|
|
511
|
+
this.stopSizeObserver();
|
|
512
|
+
if (this.pendingLayoutFrame !== null) {
|
|
513
|
+
cancelAnimationFrame(this.pendingLayoutFrame);
|
|
514
|
+
this.pendingLayoutFrame = null;
|
|
515
|
+
}
|
|
516
|
+
if (this.pendingHarmonyRetryTimer !== null) {
|
|
517
|
+
clearTimeout(this.pendingHarmonyRetryTimer);
|
|
518
|
+
this.pendingHarmonyRetryTimer = null;
|
|
519
|
+
}
|
|
520
|
+
if (this.iOSBootstrapFrame !== null) {
|
|
521
|
+
cancelAnimationFrame(this.iOSBootstrapFrame);
|
|
522
|
+
this.iOSBootstrapFrame = null;
|
|
523
|
+
}
|
|
524
|
+
this.iOSBootstrapRemaining = 0;
|
|
525
|
+
this.pendingHarmonyProps = undefined;
|
|
526
|
+
this.pendingHarmonyRetryCount = 0;
|
|
527
|
+
}
|
|
528
|
+
startSizeObserver() {
|
|
529
|
+
if (typeof ResizeObserver === "undefined") {
|
|
530
|
+
// ResizeObserver unavailable: retry mount on next frame.
|
|
531
|
+
this.scheduleMountRetry();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (this.resizeObserver)
|
|
535
|
+
return;
|
|
536
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
537
|
+
for (const entry of entries) {
|
|
538
|
+
if (entry.target !== this)
|
|
539
|
+
continue;
|
|
540
|
+
const { width, height } = entry.contentRect;
|
|
541
|
+
if (!width || !height)
|
|
542
|
+
continue;
|
|
543
|
+
if (this.mounted) {
|
|
544
|
+
this.updatePosition();
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
this.mountOrUpdate();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
this.resizeObserver.observe(this);
|
|
552
|
+
}
|
|
553
|
+
stopSizeObserver() {
|
|
554
|
+
if (this.resizeObserver) {
|
|
555
|
+
this.resizeObserver.disconnect();
|
|
556
|
+
this.resizeObserver = undefined;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
scheduleMountRetry() {
|
|
560
|
+
if (this.pendingLayoutFrame !== null || this.mounted)
|
|
561
|
+
return;
|
|
562
|
+
this.pendingLayoutFrame = requestAnimationFrame(() => {
|
|
563
|
+
this.pendingLayoutFrame = null;
|
|
564
|
+
this.mountOrUpdate();
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
scheduleIOSBootstrapFrames() {
|
|
568
|
+
if (!isIOS())
|
|
569
|
+
return;
|
|
570
|
+
// Keep a short frame-based bootstrap window so late-created WKChildScrollView
|
|
571
|
+
// can still receive mount/update without relying on hardcoded millisecond delay.
|
|
572
|
+
this.iOSBootstrapRemaining = 8;
|
|
573
|
+
const tick = () => {
|
|
574
|
+
if (!this.isConnected) {
|
|
575
|
+
this.iOSBootstrapFrame = null;
|
|
576
|
+
this.iOSBootstrapRemaining = 0;
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
this.mountOrUpdate(true);
|
|
580
|
+
this.iOSBootstrapRemaining -= 1;
|
|
581
|
+
if (this.iOSBootstrapRemaining <= 0) {
|
|
582
|
+
this.iOSBootstrapFrame = null;
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
this.iOSBootstrapFrame = requestAnimationFrame(tick);
|
|
586
|
+
};
|
|
587
|
+
this.iOSBootstrapFrame = requestAnimationFrame(tick);
|
|
588
|
+
}
|
|
589
|
+
mountOrUpdateHarmony(forceUpdate = false) {
|
|
590
|
+
if (!this.componentId)
|
|
591
|
+
return;
|
|
592
|
+
const { rect, cornerRadius } = measureElement(this);
|
|
593
|
+
const hasSize = rect.width > 0 && rect.height > 0;
|
|
594
|
+
if (!hasSize) {
|
|
595
|
+
if (!this.mounted) {
|
|
596
|
+
this.scheduleMountRetry();
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const props = this.collectProps();
|
|
601
|
+
if (cornerRadius !== undefined) {
|
|
602
|
+
props.cornerRadius = cornerRadius;
|
|
603
|
+
}
|
|
604
|
+
const zIndex = parseFloat(this.style.zIndex || "0") || 0;
|
|
605
|
+
const decision = this.updateState.decide(rect, props, zIndex, !this.mounted || forceUpdate);
|
|
606
|
+
if (!decision.shouldSend)
|
|
607
|
+
return;
|
|
608
|
+
this.ensureHarmonyEmbed(rect, props, cornerRadius, this.forceHarmonyEmbedRecreate);
|
|
609
|
+
this.forceHarmonyEmbedRecreate = false;
|
|
610
|
+
this.mounted = true;
|
|
611
|
+
}
|
|
612
|
+
ensureHarmonyEmbed(rect, props, cornerRadius, forceRecreate = false) {
|
|
613
|
+
const createEmbed = () => {
|
|
614
|
+
const embed = document.createElement("embed");
|
|
615
|
+
embed.setAttribute("type", "native/video");
|
|
616
|
+
embed.setAttribute("width", `${rect.width}`);
|
|
617
|
+
embed.setAttribute("height", `${rect.height}`);
|
|
618
|
+
embed.setAttribute("id", this.componentId);
|
|
619
|
+
embed.setAttribute("data-lx-component-id", this.componentId);
|
|
620
|
+
embed.style.display = "block";
|
|
621
|
+
embed.style.width = "100%";
|
|
622
|
+
embed.style.height = "100%";
|
|
623
|
+
embed.style.border = "none";
|
|
624
|
+
if (cornerRadius !== undefined) {
|
|
625
|
+
embed.style.borderRadius = `${cornerRadius}px`;
|
|
626
|
+
embed.style.overflow = "hidden";
|
|
627
|
+
}
|
|
628
|
+
const encodedProps = this.encodeHarmonyProps(props);
|
|
629
|
+
embed.setAttribute("src", encodedProps);
|
|
630
|
+
this.lastHarmonyProps = encodedProps;
|
|
631
|
+
return embed;
|
|
632
|
+
};
|
|
633
|
+
if (forceRecreate && this.harmonyEmbed) {
|
|
634
|
+
const oldEmbed = this.harmonyEmbed;
|
|
635
|
+
this.harmonyEmbed = undefined;
|
|
636
|
+
if (this.contains(oldEmbed)) {
|
|
637
|
+
this.removeChild(oldEmbed);
|
|
638
|
+
}
|
|
639
|
+
this.lastHarmonyProps = undefined;
|
|
640
|
+
const recreated = createEmbed();
|
|
641
|
+
this.appendChild(recreated);
|
|
642
|
+
this.harmonyEmbed = recreated;
|
|
643
|
+
this.pushHarmonyPropsUpdate(props);
|
|
644
|
+
this.pendingHarmonyRetryCount = 0;
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
// Create embed element only once - ArkWeb triggers DESTROY+CREATE on attribute changes
|
|
648
|
+
if (!this.harmonyEmbed) {
|
|
649
|
+
const embed = createEmbed();
|
|
650
|
+
this.pushHarmonyPropsUpdate(props);
|
|
651
|
+
this.appendChild(embed);
|
|
652
|
+
this.harmonyEmbed = embed;
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
// Only update props when they changed.
|
|
656
|
+
const encodedProps = this.encodeHarmonyProps(props);
|
|
657
|
+
if (encodedProps !== this.lastHarmonyProps) {
|
|
658
|
+
const pushed = this.pushHarmonyPropsUpdate(props);
|
|
659
|
+
if (pushed) {
|
|
660
|
+
this.lastHarmonyProps = encodedProps;
|
|
661
|
+
this.pendingHarmonyProps = undefined;
|
|
662
|
+
this.pendingHarmonyRetryCount = 0;
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
this.scheduleHarmonyPropsRetry(props);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// Ensure embed element matches latest corner radius for clipping
|
|
669
|
+
if (cornerRadius !== undefined && this.harmonyEmbed.style.borderRadius !== `${cornerRadius}px`) {
|
|
670
|
+
this.harmonyEmbed.style.borderRadius = `${cornerRadius}px`;
|
|
671
|
+
this.harmonyEmbed.style.overflow = "hidden";
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
encodeHarmonyProps(props) {
|
|
675
|
+
try {
|
|
676
|
+
const payload = { componentId: this.componentId, ...props };
|
|
677
|
+
return `${HARMONY_PROPS_PREFIX}${encodeURIComponent(JSON.stringify(payload))}`;
|
|
678
|
+
}
|
|
679
|
+
catch (_e) {
|
|
680
|
+
return `${HARMONY_PROPS_PREFIX}%7B%7D`;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
pushHarmonyPropsUpdate(props) {
|
|
684
|
+
if (!this.componentId || !isHarmony())
|
|
685
|
+
return false;
|
|
686
|
+
try {
|
|
687
|
+
const proxy = window.LingXiaProxy;
|
|
688
|
+
const updateFn = proxy === null || proxy === void 0 ? void 0 : proxy.nativeComponentUpdate;
|
|
689
|
+
if (typeof updateFn !== "function") {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
const payload = JSON.stringify({ componentId: this.componentId, ...props });
|
|
693
|
+
try {
|
|
694
|
+
updateFn.call(proxy, payload);
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
try {
|
|
699
|
+
updateFn.call(proxy, this.componentId, payload);
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
scheduleHarmonyPropsRetry(props) {
|
|
712
|
+
this.pendingHarmonyProps = { ...props };
|
|
713
|
+
if (this.pendingHarmonyRetryTimer !== null) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
this.pendingHarmonyRetryTimer = window.setTimeout(() => {
|
|
717
|
+
this.pendingHarmonyRetryTimer = null;
|
|
718
|
+
if (!this.isConnected || !this.pendingHarmonyProps) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const pendingProps = this.pendingHarmonyProps;
|
|
722
|
+
const pushed = this.pushHarmonyPropsUpdate(pendingProps);
|
|
723
|
+
if (pushed) {
|
|
724
|
+
this.lastHarmonyProps = this.encodeHarmonyProps(pendingProps);
|
|
725
|
+
this.pendingHarmonyProps = undefined;
|
|
726
|
+
this.pendingHarmonyRetryCount = 0;
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
this.pendingHarmonyRetryCount += 1;
|
|
730
|
+
if (this.pendingHarmonyRetryCount < 20) {
|
|
731
|
+
this.scheduleHarmonyPropsRetry(pendingProps);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}, 100);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
export function registerVideoComponent() {
|
|
738
|
+
if (!customElements.get("lx-video")) {
|
|
739
|
+
customElements.define("lx-video", LxVideoElement);
|
|
740
|
+
}
|
|
741
|
+
}
|