@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.
- package/package.json +24 -0
- package/src/adapters/es3-video-player.js +828 -0
- package/src/components/Modal.js +119 -0
- package/src/components/QtAnimationView.js +678 -0
- package/src/components/QtBaseComponent.js +165 -0
- package/src/components/QtFastListView.js +1920 -0
- package/src/components/QtFlexView.js +799 -0
- package/src/components/QtImage.js +203 -0
- package/src/components/QtItemFrame.js +239 -0
- package/src/components/QtItemStoreView.js +93 -0
- package/src/components/QtItemView.js +125 -0
- package/src/components/QtListView.js +331 -0
- package/src/components/QtLoadingView.js +55 -0
- package/src/components/QtPageRootView.js +19 -0
- package/src/components/QtPlayMark.js +168 -0
- package/src/components/QtProgressBar.js +199 -0
- package/src/components/QtQRCode.js +78 -0
- package/src/components/QtReplaceChild.js +149 -0
- package/src/components/QtRippleView.js +166 -0
- package/src/components/QtSeekBar.js +409 -0
- package/src/components/QtText.js +679 -0
- package/src/components/QtTransitionImage.js +170 -0
- package/src/components/QtView.js +706 -0
- package/src/components/QtWebView.js +613 -0
- package/src/components/TabsView.js +420 -0
- package/src/components/ViewPager.js +206 -0
- package/src/components/index.js +24 -0
- package/src/components/plugins/TextV2Component.js +70 -0
- package/src/components/plugins/index.js +7 -0
- package/src/core/SceneBuilder.js +58 -0
- package/src/core/TVFocusManager.js +2014 -0
- package/src/core/asyncLocalStorage.js +175 -0
- package/src/core/autoProxy.js +165 -0
- package/src/core/componentRegistry.js +84 -0
- package/src/core/constants.js +6 -0
- package/src/core/index.js +8 -0
- package/src/core/moduleUtils.js +36 -0
- package/src/core/patches.js +958 -0
- package/src/core/templateBinding.js +666 -0
- package/src/index.js +246 -0
- package/src/modules/AndroidDevelopModule.js +101 -0
- package/src/modules/AndroidDeviceModule.js +341 -0
- package/src/modules/AndroidNetworkModule.js +178 -0
- package/src/modules/AndroidSharedPreferencesModule.js +100 -0
- package/src/modules/ESDeviceInfoModule.js +450 -0
- package/src/modules/ESGroupDataModule.js +195 -0
- package/src/modules/ESIJKAudioPlayerModule.js +477 -0
- package/src/modules/ESLocalStorageModule.js +100 -0
- package/src/modules/ESLogModule.js +65 -0
- package/src/modules/ESModule.js +106 -0
- package/src/modules/ESNetworkSpeedModule.js +117 -0
- package/src/modules/ESToastModule.js +172 -0
- package/src/modules/EsNativeModule.js +117 -0
- package/src/modules/FastListModule.js +101 -0
- package/src/modules/FocusModule.js +145 -0
- 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
|
+
}
|