@quicktvui/web-renderer 1.0.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 (56) hide show
  1. package/package.json +24 -0
  2. package/src/adapters/es3-video-player.js +828 -0
  3. package/src/components/Modal.js +119 -0
  4. package/src/components/QtAnimationView.js +678 -0
  5. package/src/components/QtBaseComponent.js +165 -0
  6. package/src/components/QtFastListView.js +1920 -0
  7. package/src/components/QtFlexView.js +799 -0
  8. package/src/components/QtImage.js +203 -0
  9. package/src/components/QtItemFrame.js +239 -0
  10. package/src/components/QtItemStoreView.js +93 -0
  11. package/src/components/QtItemView.js +125 -0
  12. package/src/components/QtListView.js +331 -0
  13. package/src/components/QtLoadingView.js +55 -0
  14. package/src/components/QtPageRootView.js +19 -0
  15. package/src/components/QtPlayMark.js +168 -0
  16. package/src/components/QtProgressBar.js +199 -0
  17. package/src/components/QtQRCode.js +78 -0
  18. package/src/components/QtReplaceChild.js +149 -0
  19. package/src/components/QtRippleView.js +166 -0
  20. package/src/components/QtSeekBar.js +409 -0
  21. package/src/components/QtText.js +679 -0
  22. package/src/components/QtTransitionImage.js +170 -0
  23. package/src/components/QtView.js +706 -0
  24. package/src/components/QtWebView.js +613 -0
  25. package/src/components/TabsView.js +420 -0
  26. package/src/components/ViewPager.js +206 -0
  27. package/src/components/index.js +24 -0
  28. package/src/components/plugins/TextV2Component.js +70 -0
  29. package/src/components/plugins/index.js +7 -0
  30. package/src/core/SceneBuilder.js +58 -0
  31. package/src/core/TVFocusManager.js +2014 -0
  32. package/src/core/asyncLocalStorage.js +175 -0
  33. package/src/core/autoProxy.js +165 -0
  34. package/src/core/componentRegistry.js +84 -0
  35. package/src/core/constants.js +6 -0
  36. package/src/core/index.js +8 -0
  37. package/src/core/moduleUtils.js +36 -0
  38. package/src/core/patches.js +958 -0
  39. package/src/core/templateBinding.js +666 -0
  40. package/src/index.js +246 -0
  41. package/src/modules/AndroidDevelopModule.js +101 -0
  42. package/src/modules/AndroidDeviceModule.js +341 -0
  43. package/src/modules/AndroidNetworkModule.js +178 -0
  44. package/src/modules/AndroidSharedPreferencesModule.js +100 -0
  45. package/src/modules/ESDeviceInfoModule.js +450 -0
  46. package/src/modules/ESGroupDataModule.js +195 -0
  47. package/src/modules/ESIJKAudioPlayerModule.js +477 -0
  48. package/src/modules/ESLocalStorageModule.js +100 -0
  49. package/src/modules/ESLogModule.js +65 -0
  50. package/src/modules/ESModule.js +106 -0
  51. package/src/modules/ESNetworkSpeedModule.js +117 -0
  52. package/src/modules/ESToastModule.js +172 -0
  53. package/src/modules/EsNativeModule.js +117 -0
  54. package/src/modules/FastListModule.js +101 -0
  55. package/src/modules/FocusModule.js +145 -0
  56. package/src/modules/RuntimeDeviceModule.js +176 -0
@@ -0,0 +1,799 @@
1
+ // QtFlexView - A flexible layout container with data binding support
2
+ // Used by tv-flex and FastFlexView components
3
+ import { QtBaseComponent } from './QtBaseComponent'
4
+ import { registerComponent } from '../core/componentRegistry'
5
+ import {
6
+ bindTemplateDataToNode,
7
+ extractTemplateValue,
8
+ syncDomAutoWidthIn,
9
+ } from '../core/templateBinding'
10
+
11
+ // Global registry for DOM-to-Component mapping
12
+ window.__HIPPY_COMPONENT_REGISTRY__ = window.__HIPPY_COMPONENT_REGISTRY__ || new Map()
13
+
14
+ export class QtFlexView extends QtBaseComponent {
15
+ constructor(context, id, pId) {
16
+ super(context, id, pId)
17
+ this.tagName = 'FastFlexView'
18
+ this.dom = document.createElement('div')
19
+ this.dom.setAttribute('data-component-name', 'QtFlexView')
20
+
21
+ this._listData = []
22
+ this._templateChildren = []
23
+ this._itemContainer = document.createElement('div')
24
+ this._itemContainer.className = 'flex-view-item-container'
25
+ this._itemContainer.style.cssText =
26
+ 'display: flex; flex-direction: row; flex-wrap: wrap; align-content: flex-start;'
27
+
28
+ this.dom.style.cssText = `
29
+ display: flex;
30
+ flex-direction: column;
31
+ position: relative;
32
+ box-sizing: border-box;
33
+ `
34
+ // Don't append _itemContainer yet - wait for templates to be captured
35
+
36
+ // Register by id for findComponentById
37
+ registerComponent(id, this)
38
+ // Also register by dom for DOM-to-Component lookup
39
+ window.__HIPPY_COMPONENT_REGISTRY__.set(this.dom, this)
40
+
41
+ // Set up MutationObserver to watch for template children
42
+ this._observer = new MutationObserver((mutations) => {
43
+ mutations.forEach((mutation) => {
44
+ if (mutation.type === 'childList') {
45
+ mutation.addedNodes.forEach((node) => {
46
+ if (node.nodeType === Node.ELEMENT_NODE) {
47
+ this._processTemplateNode(node)
48
+ }
49
+ })
50
+ }
51
+ })
52
+ })
53
+ this._observer.observe(this.dom, { childList: true, subtree: false })
54
+ }
55
+
56
+ defaultStyle() {
57
+ return {
58
+ display: 'flex',
59
+ flexDirection: 'column',
60
+ position: 'relative',
61
+ boxSizing: 'border-box',
62
+ }
63
+ }
64
+
65
+ updateProperty(key, value) {
66
+ switch (key) {
67
+ case 'list':
68
+ console.log('[QtFlexView] updateProperty list:', value, 'isArray:', Array.isArray(value))
69
+ // Store the raw value - will be resolved when template is cloned
70
+ this._pendingListValue = value
71
+ if (Array.isArray(value)) {
72
+ this.dom.removeAttribute('list')
73
+ } else if (typeof value === 'string') {
74
+ const attrName = key
75
+ this.dom.setAttribute(attrName, value)
76
+ }
77
+ // If value is already an array, use it directly
78
+ if (Array.isArray(value)) {
79
+ this.setListData(value)
80
+ } else {
81
+ // Check if DOM has _qtListData set by parent list
82
+ requestAnimationFrame(() => {
83
+ console.log('[QtFlexView] Checking dom._qtListData:', this.dom._qtListData)
84
+ if (this.dom._qtListData && Array.isArray(this.dom._qtListData)) {
85
+ this.setListData(this.dom._qtListData)
86
+ }
87
+ })
88
+ }
89
+ break
90
+ case 'flexStyle':
91
+ if (typeof value === 'object' && value !== null) {
92
+ this._applyFlexStyle(value)
93
+ } else if (typeof value === 'string' && value.startsWith('${')) {
94
+ this.dom.setAttribute(key, value)
95
+ }
96
+ break
97
+ case 'layout':
98
+ if (typeof value === 'string' && value.startsWith('${')) {
99
+ this.dom.setAttribute(key, value)
100
+ } else {
101
+ this._updateLayout(value)
102
+ }
103
+ break
104
+ case 'clipChildren':
105
+ if (value === false) {
106
+ this.dom.style.overflow = 'visible'
107
+ }
108
+ break
109
+ case 'clipPadding':
110
+ // Handle clipPadding
111
+ break
112
+ default:
113
+ if (typeof value === 'string' && value.startsWith('${')) {
114
+ this.dom.setAttribute(key, value)
115
+ }
116
+ super.updateProperty(key, value)
117
+ }
118
+ }
119
+
120
+ // Process a template node added to the component
121
+ _processTemplateNode(node) {
122
+ console.log(
123
+ '[QtFlexView] _processTemplateNode:',
124
+ node.tagName,
125
+ node.getAttribute('data-component-name')
126
+ )
127
+ const registerTemplate = (element, component = null, id = undefined) => {
128
+ if (!this._templateChildren.some((t) => t.dom === element)) {
129
+ const type = _qtFlex_parseTemplateType(element)
130
+ const templateWrapper = {
131
+ dom: element,
132
+ tagName: element.tagName,
133
+ component,
134
+ id,
135
+ type,
136
+ }
137
+ this._templateChildren.push(templateWrapper)
138
+ element.setAttribute('data-template', 'true')
139
+ if (templateWrapper.type !== null) {
140
+ element.setAttribute('data-template-type', String(templateWrapper.type))
141
+ }
142
+ element.style.display = 'none'
143
+ console.log('[QtFlexView] Template captured, total:', this._templateChildren.length)
144
+ }
145
+ }
146
+ if (_qtFlex_parseTemplateType(node) === null && node.querySelectorAll) {
147
+ const nestedTemplates = Array.from(node.querySelectorAll('[type], [data-template-type]'))
148
+ if (nestedTemplates.length > 0) {
149
+ node.style.display = 'none'
150
+ nestedTemplates.forEach((element) => registerTemplate(element))
151
+ return
152
+ }
153
+ }
154
+ registerTemplate(node)
155
+ }
156
+
157
+ // Override insertChild to capture template children
158
+ insertChild(view, index) {
159
+ if (view && view.dom) {
160
+ const registerTemplate = (element, component = null, id = undefined) => {
161
+ if (!this._templateChildren.some((t) => t.dom === element)) {
162
+ const type = _qtFlex_parseTemplateType(element)
163
+ const templateWrapper = {
164
+ dom: element,
165
+ tagName: element.tagName,
166
+ id,
167
+ component,
168
+ type,
169
+ }
170
+ this._templateChildren.push(templateWrapper)
171
+ element.setAttribute('data-template', 'true')
172
+ if (type !== null && type !== undefined) {
173
+ element.setAttribute('data-template-type', String(type))
174
+ }
175
+ element.style.display = 'none'
176
+ }
177
+ }
178
+ if (_qtFlex_parseTemplateType(view.dom) === null && view.dom.querySelectorAll) {
179
+ const nestedTemplates = Array.from(
180
+ view.dom.querySelectorAll('[type], [data-template-type]')
181
+ )
182
+ if (nestedTemplates.length > 0) {
183
+ view.dom.style.display = 'none'
184
+ nestedTemplates.forEach((element) => registerTemplate(element))
185
+ } else {
186
+ registerTemplate(view.dom, view, view.id)
187
+ }
188
+ } else {
189
+ registerTemplate(view.dom, view, view.id)
190
+ }
191
+ if (!this.dom.contains(view.dom)) {
192
+ if (this._itemContainer && this.dom.contains(this._itemContainer)) {
193
+ this.dom.insertBefore(view.dom, this._itemContainer)
194
+ } else {
195
+ this.dom.appendChild(view.dom)
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ // Set list data and render items
202
+ setListData(data) {
203
+ console.log('[QtFlexView] setListData:', data, 'isArray:', Array.isArray(data))
204
+
205
+ // Handle wrapped params
206
+ if (Array.isArray(data) && data.length === 1 && Array.isArray(data[0])) {
207
+ data = data[0]
208
+ }
209
+
210
+ if (!Array.isArray(data)) {
211
+ console.log('[QtFlexView] setListData: not array, returning')
212
+ return
213
+ }
214
+ this._listData = data
215
+ console.log(
216
+ '[QtFlexView] _listData set:',
217
+ this._listData.length,
218
+ 'items, templates:',
219
+ this._templateChildren.length
220
+ )
221
+
222
+ // Debounce rendering to avoid multiple rapid renders
223
+ if (this._renderTimeout) {
224
+ clearTimeout(this._renderTimeout)
225
+ }
226
+
227
+ // Wait for templates to be ready, then render
228
+ const tryRender = (attempts = 0) => {
229
+ if (this._templateChildren.length > 0 || attempts >= 10) {
230
+ this._renderItems()
231
+ } else {
232
+ this._renderTimeout = setTimeout(() => tryRender(attempts + 1), 50)
233
+ }
234
+ }
235
+ this._renderTimeout = setTimeout(() => tryRender(), 0)
236
+ }
237
+
238
+ // Render items based on current data
239
+ _renderItems() {
240
+ // Ensure item container is in the DOM
241
+ if (!this.dom.contains(this._itemContainer)) {
242
+ this.dom.appendChild(this._itemContainer)
243
+ }
244
+
245
+ this._itemContainer.innerHTML = ''
246
+
247
+ if (this._listData.length === 0) {
248
+ return
249
+ }
250
+
251
+ // Hide all templates
252
+ this._templateChildren.forEach((t) => {
253
+ if (t.dom) {
254
+ t.dom.style.display = 'none'
255
+ }
256
+ })
257
+
258
+ // If no templates, create placeholder items
259
+ if (this._templateChildren.length === 0) {
260
+ this._listData.forEach((itemData, index) => {
261
+ const item = document.createElement('div')
262
+ item.setAttribute('data-index', index)
263
+ item.style.cssText =
264
+ 'width: 200px; height: 200px; margin: 10px; background: #444; display: flex; align-items: center; justify-content: center; border-radius: 8px;'
265
+
266
+ const text = document.createElement('span')
267
+ text.style.cssText = 'color: white; font-size: 14px;'
268
+ text.textContent = itemData.text || itemData.title || itemData.name || `Item ${index}`
269
+ item.appendChild(text)
270
+
271
+ this._itemContainer.appendChild(item)
272
+ })
273
+ return
274
+ }
275
+
276
+ // Create items for each data entry
277
+ this._listData.forEach((itemData, index) => {
278
+ const itemElement = this._createItemFromData(itemData, index)
279
+ if (itemElement) {
280
+ this._itemContainer.appendChild(itemElement)
281
+ }
282
+ })
283
+ }
284
+
285
+ // Create an item element by cloning template and binding data
286
+ _createItemFromData(itemData, index) {
287
+ const item = document.createElement('div')
288
+ item.setAttribute('data-index', index)
289
+ item.setAttribute('data-position', index)
290
+
291
+ if (itemData && itemData.decoration && typeof itemData.decoration === 'object') {
292
+ const dec = itemData.decoration
293
+ if (dec.top !== undefined) item.style.marginTop = dec.top + 'px'
294
+ if (dec.left !== undefined) item.style.marginLeft = dec.left + 'px'
295
+ if (dec.right !== undefined) item.style.marginRight = dec.right + 'px'
296
+ if (dec.bottom !== undefined) item.style.marginBottom = dec.bottom + 'px'
297
+ }
298
+
299
+ // Get the item's type
300
+ const itemType = itemData.type !== undefined ? itemData.type : null
301
+
302
+ // Find matching template by type
303
+ let matchingTemplate = this._templateChildren.find((t) => t.type === itemType)
304
+
305
+ const sharedTemplates = window.__QT_SHARED_TEMPLATES__
306
+ const sharedByType =
307
+ sharedTemplates && itemType !== null && itemType !== undefined
308
+ ? sharedTemplates.get(itemType)
309
+ : null
310
+
311
+ if (!matchingTemplate && sharedByType && sharedByType.dom) {
312
+ matchingTemplate = sharedByType
313
+ }
314
+
315
+ // If no matching template found, try to use template without type (default template)
316
+ if (!matchingTemplate) {
317
+ matchingTemplate = this._templateChildren.find((t) => t.type === null)
318
+ }
319
+
320
+ if (!matchingTemplate && sharedTemplates && sharedTemplates.size > 0) {
321
+ const firstShared = sharedTemplates.values().next().value
322
+ if (firstShared && firstShared.dom) {
323
+ matchingTemplate = firstShared
324
+ }
325
+ }
326
+
327
+ // If still no template, use the first template
328
+ if (!matchingTemplate && this._templateChildren.length > 0) {
329
+ matchingTemplate = this._templateChildren[0]
330
+ }
331
+
332
+ // Clone the matching template and bind data
333
+ if (matchingTemplate && matchingTemplate.dom) {
334
+ const clonedNode = matchingTemplate.dom.cloneNode(true)
335
+ clonedNode.removeAttribute('data-template')
336
+
337
+ // Reset display
338
+ clonedNode.style.display = ''
339
+ clonedNode.style.position = 'relative'
340
+ applyDomOnlyTemplateRootDefaults(clonedNode)
341
+
342
+ // Process component-level props if available
343
+ if (matchingTemplate.component && matchingTemplate.component.props) {
344
+ this._bindComponentPropsToNode(clonedNode, matchingTemplate.component.props, itemData)
345
+ }
346
+
347
+ // Bind data to the cloned node
348
+ this._bindDataToNode(clonedNode, itemData)
349
+
350
+ _fixAbsoluteChildCollapse(clonedNode)
351
+
352
+ // If clonedNode itself became absolute (e.g. via layout on root),
353
+ // the wrapper 'item' will collapse. Copy dimensions to 'item'.
354
+ if (clonedNode.style.position === 'absolute') {
355
+ if (clonedNode.style.width) item.style.width = clonedNode.style.width
356
+ if (clonedNode.style.height) item.style.height = clonedNode.style.height
357
+ }
358
+
359
+ item.appendChild(clonedNode)
360
+ }
361
+
362
+ item._itemData = itemData
363
+ item._itemPosition = index
364
+
365
+ item.addEventListener('click', () => {
366
+ this._dispatchItemClick(index, itemData)
367
+ })
368
+
369
+ return item
370
+ }
371
+
372
+ // Bind component-level props to a cloned node (handles flexStyle etc.)
373
+ _bindComponentPropsToNode(node, props, data) {
374
+ if (!props || !data) return
375
+
376
+ Object.keys(props).forEach((key) => {
377
+ const value = props[key]
378
+
379
+ if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
380
+ let resolvedValue = extractTemplateValue(value, data)
381
+ const keyLower = key.toLowerCase()
382
+
383
+ // Handle flexStyle specially
384
+ if (
385
+ (keyLower === 'flexstyle' || keyLower === 'flexstyletemplate') &&
386
+ resolvedValue &&
387
+ typeof resolvedValue === 'object'
388
+ ) {
389
+ this._applyFlexStyleToElement(node, resolvedValue)
390
+ }
391
+
392
+ // Handle textSize specially
393
+ if (keyLower === 'textsize' && resolvedValue !== undefined) {
394
+ const fontSize = typeof resolvedValue === 'number' ? resolvedValue + 'px' : resolvedValue
395
+ node.style.fontSize = fontSize
396
+ }
397
+
398
+ // Handle src attribute
399
+ if (keyLower === 'src') {
400
+ if (resolvedValue && typeof resolvedValue === 'object') {
401
+ if (resolvedValue.url !== undefined) {
402
+ resolvedValue = resolvedValue.url
403
+ } else if (resolvedValue.uri !== undefined) {
404
+ resolvedValue = resolvedValue.uri
405
+ } else if (resolvedValue.src !== undefined) {
406
+ resolvedValue = resolvedValue.src
407
+ }
408
+ }
409
+ if (node.tagName === 'IMG' && typeof resolvedValue === 'string') {
410
+ node.src = resolvedValue
411
+ }
412
+ }
413
+ }
414
+ })
415
+ }
416
+
417
+ // Bind data to a node: replace ${prop} in text and attributes
418
+ _bindDataToNode(node, data) {
419
+ bindTemplateDataToNode(node, data)
420
+ }
421
+
422
+ // Apply flexStyle object to a DOM element
423
+ _applyFlexStyleToElement(element, styleObj) {
424
+ if (!styleObj || typeof styleObj !== 'object') return
425
+
426
+ Object.keys(styleObj).forEach((key) => {
427
+ let value = styleObj[key]
428
+
429
+ if (typeof value === 'number') {
430
+ const needsPx = [
431
+ 'width',
432
+ 'height',
433
+ 'minWidth',
434
+ 'minHeight',
435
+ 'maxWidth',
436
+ 'maxHeight',
437
+ 'padding',
438
+ 'paddingTop',
439
+ 'paddingRight',
440
+ 'paddingBottom',
441
+ 'paddingLeft',
442
+ 'margin',
443
+ 'marginTop',
444
+ 'marginRight',
445
+ 'marginBottom',
446
+ 'marginLeft',
447
+ 'top',
448
+ 'left',
449
+ 'right',
450
+ 'bottom',
451
+ 'borderRadius',
452
+ 'borderWidth',
453
+ 'fontSize',
454
+ 'lineHeight',
455
+ 'letterSpacing',
456
+ 'gap',
457
+ 'rowGap',
458
+ 'columnGap',
459
+ ].includes(key)
460
+
461
+ if (needsPx) {
462
+ value = value + 'px'
463
+ }
464
+ }
465
+
466
+ try {
467
+ element.style[key] = value
468
+ } catch (e) {
469
+ try {
470
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
471
+ element.style.setProperty(cssKey, value)
472
+ } catch (err) {}
473
+ }
474
+ })
475
+ }
476
+
477
+ // Dispatch item click event
478
+ _dispatchItemClick(position, itemData) {
479
+ this.dispatchEvent('onItemClick', {
480
+ position: position,
481
+ index: position,
482
+ item: itemData,
483
+ })
484
+ }
485
+
486
+ // Update layout direction and size
487
+ _updateLayout(layout) {
488
+ // Handle layout array [x, y, width, height]
489
+ if (Array.isArray(layout)) {
490
+ const x = layout[0]
491
+ const y = layout[1]
492
+ const width = layout[2]
493
+ const height = layout[3]
494
+
495
+ if (x !== undefined && x !== null) {
496
+ this.dom.style.left = typeof x === 'number' ? `${x}px` : x
497
+ }
498
+ if (y !== undefined && y !== null) {
499
+ this.dom.style.top = typeof y === 'number' ? `${y}px` : y
500
+ }
501
+ if (width !== undefined && width !== null && width > 0) {
502
+ this.dom.style.width = typeof width === 'number' ? `${width}px` : width
503
+ }
504
+ if (height !== undefined && height !== null && height > 0) {
505
+ this.dom.style.height = typeof height === 'number' ? `${height}px` : height
506
+ }
507
+
508
+ if (
509
+ (x !== undefined || y !== undefined) &&
510
+ (!this.dom.style.position || this.dom.style.position === 'relative')
511
+ ) {
512
+ this.dom.style.position = 'absolute'
513
+ }
514
+ } else if (typeof layout === 'object' && layout !== null) {
515
+ const { x, y, width, height } = layout
516
+
517
+ if (x !== undefined && x !== null) {
518
+ this.dom.style.left = typeof x === 'number' ? `${x}px` : x
519
+ }
520
+ if (y !== undefined && y !== null) {
521
+ this.dom.style.top = typeof y === 'number' ? `${y}px` : y
522
+ }
523
+ if (width !== undefined && width !== null && width > 0) {
524
+ this.dom.style.width = typeof width === 'number' ? `${width}px` : width
525
+ }
526
+ if (height !== undefined && height !== null && height > 0) {
527
+ this.dom.style.height = typeof height === 'number' ? `${height}px` : height
528
+ }
529
+
530
+ if (
531
+ (x !== undefined || y !== undefined) &&
532
+ (!this.dom.style.position || this.dom.style.position === 'relative')
533
+ ) {
534
+ this.dom.style.position = 'absolute'
535
+ }
536
+ }
537
+
538
+ // Handle layout direction
539
+ if (layout === 'H' || layout?.direction === 'H') {
540
+ this._itemContainer.style.flexDirection = 'row'
541
+ } else if (Array.isArray(layout)) {
542
+ // Default to column layout for array format
543
+ this._itemContainer.style.flexDirection = 'column'
544
+ this._itemContainer.style.flexWrap = 'wrap'
545
+ } else {
546
+ this._itemContainer.style.flexDirection = 'column'
547
+ this._itemContainer.style.flexWrap = 'nowrap'
548
+ }
549
+ }
550
+
551
+ // Apply flexStyle object to the main container
552
+ _applyFlexStyle(styleObj) {
553
+ if (!styleObj || typeof styleObj !== 'object') return
554
+
555
+ Object.keys(styleObj).forEach((key) => {
556
+ let value = styleObj[key]
557
+
558
+ if (typeof value === 'number') {
559
+ const needsPx = [
560
+ 'width',
561
+ 'height',
562
+ 'minWidth',
563
+ 'minHeight',
564
+ 'maxWidth',
565
+ 'maxHeight',
566
+ 'padding',
567
+ 'paddingTop',
568
+ 'paddingRight',
569
+ 'paddingBottom',
570
+ 'paddingLeft',
571
+ 'margin',
572
+ 'marginTop',
573
+ 'marginRight',
574
+ 'marginBottom',
575
+ 'marginLeft',
576
+ 'top',
577
+ 'left',
578
+ 'right',
579
+ 'bottom',
580
+ 'borderRadius',
581
+ 'borderWidth',
582
+ 'fontSize',
583
+ 'lineHeight',
584
+ 'letterSpacing',
585
+ 'gap',
586
+ 'rowGap',
587
+ 'columnGap',
588
+ ].includes(key)
589
+
590
+ if (needsPx) {
591
+ value = value + 'px'
592
+ }
593
+ }
594
+
595
+ try {
596
+ if (key !== 'x' && key !== 'y') {
597
+ this.dom.style[key] = value
598
+ }
599
+ } catch (e) {
600
+ try {
601
+ if (key !== 'x' && key !== 'y') {
602
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
603
+ this.dom.style.setProperty(cssKey, value)
604
+ }
605
+ } catch (err) {}
606
+ }
607
+ })
608
+ }
609
+
610
+ // Add more items to the list
611
+ addListData(data) {
612
+ if (!Array.isArray(data)) return
613
+ const startIndex = this._listData.length
614
+ this._listData = this._listData.concat(data)
615
+ data.forEach((itemData, i) => {
616
+ const index = startIndex + i
617
+ const itemElement = this._createItemFromData(itemData, index)
618
+ if (itemElement) {
619
+ this._itemContainer.appendChild(itemElement)
620
+ }
621
+ })
622
+ }
623
+
624
+ // Clear all data
625
+ clearData() {
626
+ this._listData = []
627
+ this._itemContainer.innerHTML = ''
628
+ }
629
+ }
630
+
631
+ // ---------------------------
632
+ // DOM-only renderer for cloned templates
633
+ // ---------------------------
634
+ function _qtFlex_parseTemplateType(el) {
635
+ const raw = el.getAttribute('data-template-type') ?? el.getAttribute('type')
636
+ if (raw === null || raw === undefined) return null
637
+ return isNaN(Number(raw)) ? raw : Number(raw)
638
+ }
639
+
640
+ function _qtFlex_bindDataToNode(node, data) {
641
+ bindTemplateDataToNode(node, data)
642
+ }
643
+
644
+ export function applyDomOnlyTemplateRootDefaults(node) {
645
+ if (!node || node.nodeType !== Node.ELEMENT_NODE) return
646
+ if (node.tagName === 'DIV' && !node.style.display) {
647
+ node.style.display = 'flex'
648
+ }
649
+ if (!node.style.boxSizing) {
650
+ node.style.boxSizing = 'border-box'
651
+ }
652
+ }
653
+
654
+ function _fixAbsoluteChildCollapse(node) {
655
+ if (!node || node.nodeType !== Node.ELEMENT_NODE) return
656
+ if (!node.style.width && !node.style.height) {
657
+ let maxW = 0,
658
+ maxH = 0
659
+ for (let i = 0; i < node.children.length; i++) {
660
+ const child = node.children[i]
661
+ // Use inline style directly since node might not be attached to document yet
662
+ const pos =
663
+ child.style.position || (child.getAttribute('style') || '').includes('absolute')
664
+ ? 'absolute'
665
+ : ''
666
+ if (pos === 'absolute') {
667
+ const w = parseFloat(child.style.width) || 0
668
+ const h = parseFloat(child.style.height) || 0
669
+ if (w > maxW) maxW = w
670
+ if (h > maxH) maxH = h
671
+ }
672
+ }
673
+ if (maxW > 0) node.style.width = maxW + 'px'
674
+ if (maxH > 0) node.style.height = maxH + 'px'
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Render a tv-flex/FastFlexView subtree in-place on a plain DOM element.
680
+ * Used when parent list clones templates as DOM nodes (no component instance).
681
+ */
682
+ export function renderQtFlexViewInPlace(rootElement, listData) {
683
+ if (!rootElement) return
684
+ if (Array.isArray(listData) && listData.length === 1 && Array.isArray(listData[0])) {
685
+ listData = listData[0]
686
+ }
687
+ if (!Array.isArray(listData)) return
688
+
689
+ // Ensure item container exists
690
+ let itemContainer = null
691
+ const directChildren = Array.from(rootElement.children || [])
692
+ for (const child of directChildren) {
693
+ if (child.classList && child.classList.contains('flex-view-item-container')) {
694
+ itemContainer = child
695
+ break
696
+ }
697
+ }
698
+ if (!itemContainer) {
699
+ itemContainer = document.createElement('div')
700
+ itemContainer.className = 'flex-view-item-container'
701
+ itemContainer.style.cssText =
702
+ 'display: flex; flex-direction: row; flex-wrap: wrap; align-content: flex-start;'
703
+ rootElement.appendChild(itemContainer)
704
+ }
705
+
706
+ // Collect templates from direct children (exclude item container)
707
+ const templateElements = Array.from(rootElement.children || []).filter(
708
+ (el) => el !== itemContainer
709
+ )
710
+ const templates = []
711
+ templateElements.forEach((el) => {
712
+ const ownType = _qtFlex_parseTemplateType(el)
713
+ if (ownType !== null) {
714
+ el.setAttribute('data-template', 'true')
715
+ el.style.display = 'none'
716
+ el.setAttribute('data-template-type', String(ownType))
717
+ templates.push({ dom: el, type: ownType })
718
+ return
719
+ }
720
+ const nestedTemplates = el.querySelectorAll
721
+ ? Array.from(el.querySelectorAll('[type], [data-template-type]'))
722
+ : []
723
+ if (nestedTemplates.length > 0) {
724
+ el.style.display = 'none'
725
+ nestedTemplates.forEach((nested) => {
726
+ const type = _qtFlex_parseTemplateType(nested)
727
+ nested.setAttribute('data-template', 'true')
728
+ nested.style.display = 'none'
729
+ if (type !== null) nested.setAttribute('data-template-type', String(type))
730
+ templates.push({ dom: nested, type })
731
+ })
732
+ return
733
+ }
734
+ el.setAttribute('data-template', 'true')
735
+ el.style.display = 'none'
736
+ templates.push({ dom: el, type: null })
737
+ })
738
+
739
+ itemContainer.innerHTML = ''
740
+ if (listData.length === 0) return
741
+ if (templates.length === 0) {
742
+ const sharedTemplates = window.__QT_SHARED_TEMPLATES__
743
+ if (sharedTemplates && sharedTemplates.size > 0) {
744
+ const shared = Array.from(sharedTemplates.values()).filter((t) => t && t.dom)
745
+ if (shared.length > 0) {
746
+ shared.forEach((t) => {
747
+ t.dom.setAttribute('data-template', 'true')
748
+ t.dom.style.display = 'none'
749
+ const type = _qtFlex_parseTemplateType(t.dom)
750
+ if (type !== null) t.dom.setAttribute('data-template-type', String(type))
751
+ templates.push({ dom: t.dom, type })
752
+ })
753
+ }
754
+ }
755
+ }
756
+ if (templates.length === 0) return
757
+
758
+ listData.forEach((itemData, index) => {
759
+ const itemWrapper = document.createElement('div')
760
+ itemWrapper.setAttribute('data-index', index)
761
+ itemWrapper.setAttribute('data-position', index)
762
+ itemWrapper._itemData = itemData
763
+ itemWrapper._itemPosition = index
764
+
765
+ if (itemData && itemData.decoration && typeof itemData.decoration === 'object') {
766
+ const dec = itemData.decoration
767
+ if (dec.top !== undefined) itemWrapper.style.marginTop = dec.top + 'px'
768
+ if (dec.left !== undefined) itemWrapper.style.marginLeft = dec.left + 'px'
769
+ if (dec.right !== undefined) itemWrapper.style.marginRight = dec.right + 'px'
770
+ if (dec.bottom !== undefined) itemWrapper.style.marginBottom = dec.bottom + 'px'
771
+ }
772
+
773
+ const itemType = itemData && itemData.type !== undefined ? itemData.type : null
774
+ let matching = templates.find((t) => t.type === itemType)
775
+ if (!matching) matching = templates.find((t) => t.type === null)
776
+ if (!matching) matching = templates[0]
777
+
778
+ const clonedNode = matching.dom.cloneNode(true)
779
+ clonedNode.removeAttribute('data-template')
780
+ clonedNode.style.display = ''
781
+ clonedNode.style.position = 'relative'
782
+ applyDomOnlyTemplateRootDefaults(clonedNode)
783
+
784
+ _qtFlex_bindDataToNode(clonedNode, itemData)
785
+ _fixAbsoluteChildCollapse(clonedNode)
786
+
787
+ // If clonedNode itself became absolute (e.g. via layout on root),
788
+ // the wrapper 'itemWrapper' will collapse. Copy dimensions.
789
+ if (clonedNode.style.position === 'absolute') {
790
+ if (clonedNode.style.width) itemWrapper.style.width = clonedNode.style.width
791
+ if (clonedNode.style.height) itemWrapper.style.height = clonedNode.style.height
792
+ }
793
+
794
+ itemWrapper.appendChild(clonedNode)
795
+ itemContainer.appendChild(itemWrapper)
796
+ })
797
+
798
+ requestAnimationFrame(() => syncDomAutoWidthIn(itemContainer))
799
+ }