@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,1920 @@
|
|
|
1
|
+
// QtFastListView - A data-driven list component
|
|
2
|
+
// Children act as templates, data is bound via ${prop} syntax
|
|
3
|
+
import { QtBaseComponent } from './QtBaseComponent'
|
|
4
|
+
import { registerComponent } from '../core/componentRegistry'
|
|
5
|
+
import { applyDomOnlyTemplateRootDefaults, renderQtFlexViewInPlace } from './QtFlexView'
|
|
6
|
+
import {
|
|
7
|
+
bindTemplateDataToNode,
|
|
8
|
+
extractTemplateValue,
|
|
9
|
+
replaceTemplatePlaceholders,
|
|
10
|
+
syncDomAutoWidthIn,
|
|
11
|
+
} from '../core/templateBinding'
|
|
12
|
+
import {
|
|
13
|
+
registerFastListInstance,
|
|
14
|
+
unregisterFastListInstance,
|
|
15
|
+
getFadeConfig,
|
|
16
|
+
} from '../modules/FastListModule'
|
|
17
|
+
|
|
18
|
+
export class QtFastListView extends QtBaseComponent {
|
|
19
|
+
constructor(context, id, pId) {
|
|
20
|
+
super(context, id, pId)
|
|
21
|
+
this.tagName = 'FastListView'
|
|
22
|
+
this.dom = document.createElement('div')
|
|
23
|
+
this.dom.setAttribute('data-component-name', 'FastListView')
|
|
24
|
+
this._listData = []
|
|
25
|
+
this._templateChildren = [] // Regular templates (cloned for each data item)
|
|
26
|
+
this._singletonTemplates = [] // Singleton templates (used once, not cloned)
|
|
27
|
+
this._horizontal = false // Default: vertical layout
|
|
28
|
+
this._spanCount = 0 // Default: list layout (0 = list, > 0 = grid)
|
|
29
|
+
this._fadeConfig = getFadeConfig() // Initialize fade config
|
|
30
|
+
this._focusedIndex = -1
|
|
31
|
+
this._itemContainer = document.createElement('div')
|
|
32
|
+
this._itemContainer.className = 'fast-list-item-container'
|
|
33
|
+
this._itemContainer.style.cssText =
|
|
34
|
+
'display: flex; flex-direction: column; align-content: flex-start;'
|
|
35
|
+
this.dom.appendChild(this._itemContainer)
|
|
36
|
+
this.dom.style.cssText = 'overflow: auto; display: block;'
|
|
37
|
+
// 添加实例引用,用于焦点导航
|
|
38
|
+
this.dom.__fastListViewInstance = this
|
|
39
|
+
this._itemContainer.__fastListViewInstance = this
|
|
40
|
+
this._templateCaptured = false
|
|
41
|
+
|
|
42
|
+
registerComponent(id, this)
|
|
43
|
+
|
|
44
|
+
// Register this instance for fade config updates
|
|
45
|
+
registerFastListInstance(this)
|
|
46
|
+
|
|
47
|
+
// Set up MutationObserver to watch for children added to DOM
|
|
48
|
+
this._observer = new MutationObserver((mutations) => {
|
|
49
|
+
mutations.forEach((mutation) => {
|
|
50
|
+
if (mutation.type === 'childList') {
|
|
51
|
+
mutation.addedNodes.forEach((node) => {
|
|
52
|
+
if (node !== this._itemContainer && node.nodeType === Node.ELEMENT_NODE) {
|
|
53
|
+
this._processTemplateNode(node)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
this._observer.observe(this.dom, { childList: true, subtree: false })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Update fade configuration (called by FastListModule)
|
|
64
|
+
* @param {{enabled: boolean, duration: number}} config
|
|
65
|
+
*/
|
|
66
|
+
updateFadeConfig(config) {
|
|
67
|
+
this._fadeConfig = { ...config }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
defaultStyle() {
|
|
71
|
+
return {
|
|
72
|
+
display: 'block',
|
|
73
|
+
overflow: 'auto',
|
|
74
|
+
boxSizing: 'border-box',
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_processTemplateNode(node) {
|
|
79
|
+
if (!node || node === this._itemContainer) return
|
|
80
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return
|
|
81
|
+
|
|
82
|
+
const parseType = (element) => {
|
|
83
|
+
const raw = element.getAttribute('data-template-type') ?? element.getAttribute('type')
|
|
84
|
+
if (raw === null || raw === undefined || raw === '') return null
|
|
85
|
+
return isNaN(Number(raw)) ? raw : Number(raw)
|
|
86
|
+
}
|
|
87
|
+
const registerTemplate = (element, singleton = false) => {
|
|
88
|
+
const type = parseType(element)
|
|
89
|
+
if (singleton) {
|
|
90
|
+
if (!this._singletonTemplates.some((t) => t.dom === element)) {
|
|
91
|
+
this._singletonTemplates.push({
|
|
92
|
+
dom: element,
|
|
93
|
+
tagName: element.tagName,
|
|
94
|
+
component: null,
|
|
95
|
+
type,
|
|
96
|
+
})
|
|
97
|
+
element.setAttribute('data-singleton', 'true')
|
|
98
|
+
}
|
|
99
|
+
element.style.display = 'none'
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (!this._templateChildren.some((t) => t.dom === element)) {
|
|
103
|
+
this._templateChildren.push({
|
|
104
|
+
dom: element,
|
|
105
|
+
tagName: element.tagName,
|
|
106
|
+
component: null,
|
|
107
|
+
type,
|
|
108
|
+
})
|
|
109
|
+
element.setAttribute('data-template', 'true')
|
|
110
|
+
if (type !== null) {
|
|
111
|
+
element.setAttribute('data-template-type', String(type))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
element.style.display = 'none'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const hasSingleton = node.hasAttribute && node.hasAttribute('singleton')
|
|
118
|
+
const type = parseType(node)
|
|
119
|
+
|
|
120
|
+
if (!hasSingleton && type === null && node.querySelectorAll) {
|
|
121
|
+
const nestedTemplates = Array.from(
|
|
122
|
+
node.querySelectorAll('[type], [data-template-type], [singleton]')
|
|
123
|
+
)
|
|
124
|
+
if (nestedTemplates.length > 0) {
|
|
125
|
+
node.style.display = 'none'
|
|
126
|
+
nestedTemplates.forEach((element) => {
|
|
127
|
+
registerTemplate(element, element.hasAttribute('singleton'))
|
|
128
|
+
})
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (hasSingleton) {
|
|
134
|
+
registerTemplate(node, true)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
registerTemplate(node)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Handle property updates
|
|
142
|
+
updateProperty(key, value) {
|
|
143
|
+
switch (key) {
|
|
144
|
+
case 'horizontal':
|
|
145
|
+
this._horizontal = value === true || value === 'true' || value === ''
|
|
146
|
+
if (this._horizontal) {
|
|
147
|
+
this.dom.setAttribute('horizontal', '')
|
|
148
|
+
} else {
|
|
149
|
+
this.dom.removeAttribute('horizontal')
|
|
150
|
+
}
|
|
151
|
+
this._updateLayoutDirection()
|
|
152
|
+
break
|
|
153
|
+
case 'spanCount':
|
|
154
|
+
case 'span-count':
|
|
155
|
+
this._spanCount = value > 0 ? value : 0
|
|
156
|
+
if (this._spanCount > 0) {
|
|
157
|
+
this.dom.setAttribute('span-count', String(this._spanCount))
|
|
158
|
+
} else {
|
|
159
|
+
this.dom.removeAttribute('span-count')
|
|
160
|
+
}
|
|
161
|
+
this._updateLayoutDirection()
|
|
162
|
+
break
|
|
163
|
+
case 'listData':
|
|
164
|
+
case 'list-data':
|
|
165
|
+
this.setListData(value)
|
|
166
|
+
break
|
|
167
|
+
case 'list':
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
this.setListData(value)
|
|
170
|
+
} else if (typeof value === 'string' && value.startsWith('${')) {
|
|
171
|
+
// Template variable - store for later resolution
|
|
172
|
+
this.dom.setAttribute('list', value)
|
|
173
|
+
this._pendingListValue = value
|
|
174
|
+
} else {
|
|
175
|
+
this.setListData(value)
|
|
176
|
+
}
|
|
177
|
+
break
|
|
178
|
+
default:
|
|
179
|
+
if (typeof value === 'string' && value.startsWith('${')) {
|
|
180
|
+
this.dom.setAttribute(key, value)
|
|
181
|
+
}
|
|
182
|
+
super.updateProperty(key, value)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Update layout direction based on horizontal and spanCount properties
|
|
187
|
+
_updateLayoutDirection() {
|
|
188
|
+
if (this._spanCount > 0) {
|
|
189
|
+
// Grid layout: use CSS Grid
|
|
190
|
+
this._itemContainer.style.display = 'grid'
|
|
191
|
+
this._itemContainer.style.gridTemplateColumns = `repeat(${this._spanCount}, 1fr)`
|
|
192
|
+
this._itemContainer.style.flexDirection = ''
|
|
193
|
+
this._itemContainer.style.flexWrap = ''
|
|
194
|
+
this._itemContainer.style.overflowX = 'hidden'
|
|
195
|
+
this._itemContainer.style.overflowY = 'auto'
|
|
196
|
+
} else if (this._horizontal) {
|
|
197
|
+
// Horizontal list: row layout, no wrap
|
|
198
|
+
this._itemContainer.style.display = 'flex'
|
|
199
|
+
this._itemContainer.style.flexDirection = 'row'
|
|
200
|
+
this._itemContainer.style.flexWrap = 'nowrap'
|
|
201
|
+
this._itemContainer.style.gridTemplateColumns = ''
|
|
202
|
+
this._itemContainer.style.overflowX = 'auto'
|
|
203
|
+
this._itemContainer.style.overflowY = 'hidden'
|
|
204
|
+
} else {
|
|
205
|
+
// Vertical list: column layout
|
|
206
|
+
this._itemContainer.style.display = 'flex'
|
|
207
|
+
this._itemContainer.style.flexDirection = 'column'
|
|
208
|
+
this._itemContainer.style.flexWrap = 'nowrap'
|
|
209
|
+
this._itemContainer.style.gridTemplateColumns = ''
|
|
210
|
+
this._itemContainer.style.overflowX = 'hidden'
|
|
211
|
+
this._itemContainer.style.overflowY = 'auto'
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Helper: check if a view is singleton
|
|
216
|
+
_isSingleton(view) {
|
|
217
|
+
if (view.props && view.props.singleton) return true
|
|
218
|
+
if (view.dom && view.dom.hasAttribute && view.dom.hasAttribute('singleton')) return true
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Override insertChild to capture template children
|
|
223
|
+
insertChild(view, index) {
|
|
224
|
+
if (view && view.dom) {
|
|
225
|
+
// Check if this view's parent is NOT this FastListView (meaning it should be nested)
|
|
226
|
+
// This happens when a child view has a pId that matches one of our templates
|
|
227
|
+
if (view.pId && view.pId !== this.id) {
|
|
228
|
+
const parentTemplate =
|
|
229
|
+
this._singletonTemplates.find((t) => t.component && t.component.id === view.pId) ||
|
|
230
|
+
this._templateChildren.find((t) => t.component && t.component.id === view.pId)
|
|
231
|
+
if (parentTemplate) {
|
|
232
|
+
if (parentTemplate.component && parentTemplate.component.insertChild) {
|
|
233
|
+
parentTemplate.component.insertChild(view, index)
|
|
234
|
+
}
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check if this is a singleton template
|
|
240
|
+
if (this._isSingleton(view)) {
|
|
241
|
+
if (!this._singletonTemplates.some((t) => t.dom === view.dom)) {
|
|
242
|
+
// Extract type from the view's props or dom (same logic as regular templates)
|
|
243
|
+
let type = null
|
|
244
|
+
if (view.props && view.props.type !== undefined) {
|
|
245
|
+
type = view.props.type
|
|
246
|
+
} else if (view.dom.hasAttribute('type')) {
|
|
247
|
+
const typeAttr = view.dom.getAttribute('type')
|
|
248
|
+
type =
|
|
249
|
+
typeAttr !== null ? (isNaN(Number(typeAttr)) ? typeAttr : Number(typeAttr)) : null
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this._singletonTemplates.push({
|
|
253
|
+
dom: view.dom,
|
|
254
|
+
tagName: view.tagName,
|
|
255
|
+
id: view.id,
|
|
256
|
+
component: view,
|
|
257
|
+
type: type,
|
|
258
|
+
})
|
|
259
|
+
view.dom.setAttribute('data-singleton', 'true')
|
|
260
|
+
}
|
|
261
|
+
view.dom.style.display = 'none'
|
|
262
|
+
// Don't add to templateChildren, and don't return
|
|
263
|
+
// Let it continue so child elements can be mounted properly
|
|
264
|
+
} else {
|
|
265
|
+
// Regular template (not singleton)
|
|
266
|
+
if (!this._templateChildren.some((t) => t.dom === view.dom)) {
|
|
267
|
+
// Extract type from the view's props or dom
|
|
268
|
+
let type = null
|
|
269
|
+
if (view.props && view.props.type !== undefined) {
|
|
270
|
+
type = view.props.type
|
|
271
|
+
} else if (view.dom.hasAttribute('type')) {
|
|
272
|
+
const typeAttr = view.dom.getAttribute('type')
|
|
273
|
+
type =
|
|
274
|
+
typeAttr !== null ? (isNaN(Number(typeAttr)) ? typeAttr : Number(typeAttr)) : null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const templateWrapper = {
|
|
278
|
+
dom: view.dom,
|
|
279
|
+
tagName: view.tagName,
|
|
280
|
+
id: view.id,
|
|
281
|
+
component: view,
|
|
282
|
+
type: type,
|
|
283
|
+
}
|
|
284
|
+
this._templateChildren.push(templateWrapper)
|
|
285
|
+
view.dom.setAttribute('data-template', 'true')
|
|
286
|
+
}
|
|
287
|
+
view.dom.style.display = 'none'
|
|
288
|
+
}
|
|
289
|
+
if (!this.dom.contains(view.dom)) {
|
|
290
|
+
if (this._itemContainer && this.dom.contains(this._itemContainer)) {
|
|
291
|
+
this.dom.insertBefore(view.dom, this._itemContainer)
|
|
292
|
+
} else {
|
|
293
|
+
this.dom.appendChild(view.dom)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (this._listData.length > 0 && !this._rerenderScheduled) {
|
|
298
|
+
this._rerenderScheduled = true
|
|
299
|
+
requestAnimationFrame(() => {
|
|
300
|
+
this._rerenderScheduled = false
|
|
301
|
+
this._renderItems()
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Override beforeChildMount - called before a child is mounted
|
|
308
|
+
async beforeChildMount(child, childPosition) {
|
|
309
|
+
if (child && child.dom) {
|
|
310
|
+
// Check if this child's parent is NOT this FastListView
|
|
311
|
+
if (child.pId && child.pId !== this.id) {
|
|
312
|
+
const parentTemplate =
|
|
313
|
+
this._singletonTemplates.find((t) => t.component && t.component.id === child.pId) ||
|
|
314
|
+
this._templateChildren.find((t) => t.component && t.component.id === child.pId)
|
|
315
|
+
if (parentTemplate) {
|
|
316
|
+
if (parentTemplate.component && parentTemplate.component.beforeChildMount) {
|
|
317
|
+
await parentTemplate.component.beforeChildMount(child, childPosition)
|
|
318
|
+
}
|
|
319
|
+
await super.beforeChildMount(child, childPosition)
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Check if this is a singleton template
|
|
325
|
+
if (this._isSingleton(child)) {
|
|
326
|
+
if (!this._singletonTemplates.some((t) => t.dom === child.dom)) {
|
|
327
|
+
// Extract type from the child's props or dom (same logic as regular templates)
|
|
328
|
+
let type = null
|
|
329
|
+
if (child.props && child.props.type !== undefined) {
|
|
330
|
+
type = child.props.type
|
|
331
|
+
} else if (child.dom.hasAttribute('type')) {
|
|
332
|
+
const typeAttr = child.dom.getAttribute('type')
|
|
333
|
+
type =
|
|
334
|
+
typeAttr !== null ? (isNaN(Number(typeAttr)) ? typeAttr : Number(typeAttr)) : null
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this._singletonTemplates.push({
|
|
338
|
+
dom: child.dom,
|
|
339
|
+
tagName: child.tagName,
|
|
340
|
+
id: child.id,
|
|
341
|
+
component: child,
|
|
342
|
+
type: type,
|
|
343
|
+
})
|
|
344
|
+
child.dom.setAttribute('data-singleton', 'true')
|
|
345
|
+
}
|
|
346
|
+
child.dom.style.display = 'none'
|
|
347
|
+
// Don't return - let child elements continue to be mounted
|
|
348
|
+
} else {
|
|
349
|
+
// Regular template (not singleton)
|
|
350
|
+
if (!this._templateChildren.some((t) => t.dom === child.dom)) {
|
|
351
|
+
// Extract type from the child's props or dom
|
|
352
|
+
let type = null
|
|
353
|
+
if (child.props && child.props.type !== undefined) {
|
|
354
|
+
type = child.props.type
|
|
355
|
+
} else if (child.dom.hasAttribute('type')) {
|
|
356
|
+
const typeAttr = child.dom.getAttribute('type')
|
|
357
|
+
type =
|
|
358
|
+
typeAttr !== null ? (isNaN(Number(typeAttr)) ? typeAttr : Number(typeAttr)) : null
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const templateWrapper = {
|
|
362
|
+
dom: child.dom,
|
|
363
|
+
tagName: child.tagName,
|
|
364
|
+
id: child.id,
|
|
365
|
+
component: child,
|
|
366
|
+
type: type,
|
|
367
|
+
}
|
|
368
|
+
this._templateChildren.push(templateWrapper)
|
|
369
|
+
child.dom.setAttribute('data-template', 'true')
|
|
370
|
+
}
|
|
371
|
+
child.dom.style.display = 'none'
|
|
372
|
+
}
|
|
373
|
+
if (!this.dom.contains(child.dom)) {
|
|
374
|
+
if (this._itemContainer && this.dom.contains(this._itemContainer)) {
|
|
375
|
+
this.dom.insertBefore(child.dom, this._itemContainer)
|
|
376
|
+
} else {
|
|
377
|
+
this.dom.appendChild(child.dom)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
await super.beforeChildMount(child, childPosition)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Capture templates from DOM children
|
|
385
|
+
_captureTemplatesFromDOM() {
|
|
386
|
+
this._templateCaptured = true
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Set list data and render items
|
|
390
|
+
setListData(data) {
|
|
391
|
+
// Handle wrapped params - sometimes data comes as [dataArray]
|
|
392
|
+
if (Array.isArray(data) && data.length === 1 && Array.isArray(data[0])) {
|
|
393
|
+
data = data[0]
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!Array.isArray(data)) {
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
this._listData = data
|
|
400
|
+
|
|
401
|
+
// Wait for templates to be ready, then render
|
|
402
|
+
const tryRender = (attempts = 0) => {
|
|
403
|
+
if (
|
|
404
|
+
this._templateChildren.length > 0 ||
|
|
405
|
+
this._singletonTemplates.length > 0 ||
|
|
406
|
+
attempts >= 10
|
|
407
|
+
) {
|
|
408
|
+
this._renderItems()
|
|
409
|
+
} else {
|
|
410
|
+
setTimeout(() => tryRender(attempts + 1), 50)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
setTimeout(() => tryRender(), 0)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Render items based on current data
|
|
417
|
+
_renderItems() {
|
|
418
|
+
this._itemContainer.innerHTML = ''
|
|
419
|
+
|
|
420
|
+
// Hide regular templates (they will be cloned)
|
|
421
|
+
this._templateChildren.forEach((t) => {
|
|
422
|
+
if (t.dom) {
|
|
423
|
+
t.dom.style.display = 'none'
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
const renderedSingletonTypes = new Set()
|
|
428
|
+
|
|
429
|
+
// Process singleton templates - show directly, no cloning
|
|
430
|
+
// Match by type instead of index
|
|
431
|
+
this._singletonTemplates.forEach((template) => {
|
|
432
|
+
if (template.dom) {
|
|
433
|
+
// Find matching data by type
|
|
434
|
+
let matchingData = null
|
|
435
|
+
if (template.type !== null && template.type !== undefined) {
|
|
436
|
+
// Find data with matching type
|
|
437
|
+
matchingData = this._listData.find((item) => item.type === template.type)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// If no matching data found, hide this template (don't render it)
|
|
441
|
+
if (!matchingData) {
|
|
442
|
+
template.dom.style.display = 'none'
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Show the singleton
|
|
447
|
+
template.dom.style.display = ''
|
|
448
|
+
|
|
449
|
+
if (matchingData.type !== null && matchingData.type !== undefined) {
|
|
450
|
+
renderedSingletonTypes.add(matchingData.type)
|
|
451
|
+
} else if (template.type !== null && template.type !== undefined) {
|
|
452
|
+
renderedSingletonTypes.add(template.type)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Apply style from matching data if available
|
|
456
|
+
if (matchingData.style) {
|
|
457
|
+
this._applyStyleToElement(template.dom, matchingData.style)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Move to item container
|
|
461
|
+
this._itemContainer.appendChild(template.dom)
|
|
462
|
+
}
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
if (this._listData.length === 0) {
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Create items for each data entry (for regular templates)
|
|
470
|
+
this._listData.forEach((itemData, index) => {
|
|
471
|
+
if (renderedSingletonTypes.has(itemData.type)) return
|
|
472
|
+
const itemElement = this._createItemFromData(itemData, index)
|
|
473
|
+
if (itemElement) {
|
|
474
|
+
this._itemContainer.appendChild(itemElement)
|
|
475
|
+
}
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
// Initialize showOnState visibility for dynamically created elements
|
|
479
|
+
requestAnimationFrame(() => {
|
|
480
|
+
const focusManager = global.__TV_FOCUS_MANAGER__
|
|
481
|
+
if (focusManager && focusManager.initShowOnStateForElement) {
|
|
482
|
+
focusManager.initShowOnStateForElement(this._itemContainer)
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Helper: apply style object to element
|
|
488
|
+
_applyStyleToElement(element, styleObj) {
|
|
489
|
+
if (!styleObj || typeof styleObj !== 'object') return
|
|
490
|
+
Object.keys(styleObj).forEach((key) => {
|
|
491
|
+
let value = styleObj[key]
|
|
492
|
+
if (typeof value === 'number') {
|
|
493
|
+
const needsPx = [
|
|
494
|
+
'width',
|
|
495
|
+
'height',
|
|
496
|
+
'minWidth',
|
|
497
|
+
'minHeight',
|
|
498
|
+
'maxWidth',
|
|
499
|
+
'maxHeight',
|
|
500
|
+
'padding',
|
|
501
|
+
'paddingTop',
|
|
502
|
+
'paddingRight',
|
|
503
|
+
'paddingBottom',
|
|
504
|
+
'paddingLeft',
|
|
505
|
+
'margin',
|
|
506
|
+
'marginTop',
|
|
507
|
+
'marginRight',
|
|
508
|
+
'marginBottom',
|
|
509
|
+
'marginLeft',
|
|
510
|
+
'top',
|
|
511
|
+
'left',
|
|
512
|
+
'right',
|
|
513
|
+
'bottom',
|
|
514
|
+
'borderRadius',
|
|
515
|
+
'borderWidth',
|
|
516
|
+
].includes(key)
|
|
517
|
+
if (needsPx) value = value + 'px'
|
|
518
|
+
}
|
|
519
|
+
element.style[key] = value
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Create an item element by cloning template and binding data
|
|
524
|
+
_createItemFromData(itemData, index) {
|
|
525
|
+
const item = document.createElement('div')
|
|
526
|
+
item.setAttribute('data-index', index)
|
|
527
|
+
item.setAttribute('data-position', index)
|
|
528
|
+
|
|
529
|
+
// Get the item's type
|
|
530
|
+
const itemType = itemData.type !== undefined ? itemData.type : null
|
|
531
|
+
|
|
532
|
+
// Find matching template by type
|
|
533
|
+
let matchingTemplate = this._templateChildren.find((t) => t.type === itemType)
|
|
534
|
+
|
|
535
|
+
const sharedTemplates = window.__QT_SHARED_TEMPLATES__
|
|
536
|
+
const sharedByType =
|
|
537
|
+
sharedTemplates && itemType !== null && itemType !== undefined
|
|
538
|
+
? sharedTemplates.get(itemType)
|
|
539
|
+
: null
|
|
540
|
+
|
|
541
|
+
if (!matchingTemplate && sharedByType && sharedByType.dom) {
|
|
542
|
+
matchingTemplate = sharedByType
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// If no matching template found, try to use template without type (default template)
|
|
546
|
+
if (!matchingTemplate) {
|
|
547
|
+
matchingTemplate = this._templateChildren.find((t) => t.type === null)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (!matchingTemplate && sharedTemplates && sharedTemplates.size > 0) {
|
|
551
|
+
const firstShared = sharedTemplates.values().next().value
|
|
552
|
+
if (firstShared && firstShared.dom) {
|
|
553
|
+
matchingTemplate = firstShared
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// If still no template, use the first template
|
|
558
|
+
if (!matchingTemplate && this._templateChildren.length > 0) {
|
|
559
|
+
matchingTemplate = this._templateChildren[0]
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Clone the matching template and bind data
|
|
563
|
+
if (matchingTemplate && matchingTemplate.dom) {
|
|
564
|
+
const clonedNode = matchingTemplate.dom.cloneNode(true)
|
|
565
|
+
clonedNode.removeAttribute('data-template')
|
|
566
|
+
|
|
567
|
+
// Reset position for display
|
|
568
|
+
clonedNode.style.position = 'relative'
|
|
569
|
+
clonedNode.style.left = 'auto'
|
|
570
|
+
clonedNode.style.top = 'auto'
|
|
571
|
+
clonedNode.style.display = ''
|
|
572
|
+
|
|
573
|
+
// Process component-level props if available
|
|
574
|
+
// console.log(
|
|
575
|
+
// '[QtFastListView] matchingTemplate:',
|
|
576
|
+
// matchingTemplate.tagName,
|
|
577
|
+
// 'has component:',
|
|
578
|
+
// !!matchingTemplate.component,
|
|
579
|
+
// 'has props:',
|
|
580
|
+
// !!matchingTemplate.component?.props
|
|
581
|
+
// )
|
|
582
|
+
if (matchingTemplate.component && matchingTemplate.component.props) {
|
|
583
|
+
// console.log('[QtFastListView] props keys:', Object.keys(matchingTemplate.component.props))
|
|
584
|
+
this._bindComponentPropsToNode(clonedNode, matchingTemplate.component.props, itemData, {
|
|
585
|
+
itemList: this._listData,
|
|
586
|
+
})
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Bind data to the cloned node, pass list data as additional scope
|
|
590
|
+
this._bindDataToNode(clonedNode, itemData, { itemList: this._listData })
|
|
591
|
+
|
|
592
|
+
item.appendChild(clonedNode)
|
|
593
|
+
|
|
594
|
+
// Process child list data after DOM is attached
|
|
595
|
+
requestAnimationFrame(() => {
|
|
596
|
+
this._processChildListData(item)
|
|
597
|
+
syncDomAutoWidthIn(item)
|
|
598
|
+
})
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// If no templates, create a simple item
|
|
602
|
+
if (this._templateChildren.length === 0) {
|
|
603
|
+
const textContent = itemData.text || itemData.title || itemData.name || `Item ${index}`
|
|
604
|
+
const textSpan = document.createElement('span')
|
|
605
|
+
textSpan.textContent = textContent
|
|
606
|
+
textSpan.style.cssText =
|
|
607
|
+
'color: white; font-size: 14px; padding: 5px; background: #333; margin: 5px; border-radius: 4px;'
|
|
608
|
+
item.appendChild(textSpan)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Handle decoration (positioning)
|
|
612
|
+
if (itemData.decoration) {
|
|
613
|
+
const dec = itemData.decoration
|
|
614
|
+
if (dec.top !== undefined) item.style.marginTop = dec.top + 'px'
|
|
615
|
+
if (dec.left !== undefined) item.style.marginLeft = dec.left + 'px'
|
|
616
|
+
if (dec.right !== undefined) item.style.marginRight = dec.right + 'px'
|
|
617
|
+
if (dec.bottom !== undefined) item.style.marginBottom = dec.bottom + 'px'
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
item._itemData = itemData
|
|
621
|
+
item._itemPosition = index
|
|
622
|
+
|
|
623
|
+
item.addEventListener('click', (evt) => {
|
|
624
|
+
const nested = this._findNestedItemMeta(evt?.target, item)
|
|
625
|
+
if (
|
|
626
|
+
nested &&
|
|
627
|
+
nested.itemData !== itemData &&
|
|
628
|
+
itemData &&
|
|
629
|
+
typeof itemData === 'object' &&
|
|
630
|
+
Array.isArray(itemData.itemList)
|
|
631
|
+
) {
|
|
632
|
+
this._dispatchItemClick(nested.position, nested.itemData, index)
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
this._dispatchItemClick(index, itemData)
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
// Apply fade-in animation if enabled
|
|
639
|
+
if (this._fadeConfig.enabled) {
|
|
640
|
+
item.style.opacity = '0'
|
|
641
|
+
item.style.transition = `opacity ${this._fadeConfig.duration}ms ease-in-out`
|
|
642
|
+
// Trigger animation after element is in DOM
|
|
643
|
+
requestAnimationFrame(() => {
|
|
644
|
+
requestAnimationFrame(() => {
|
|
645
|
+
item.style.opacity = '1'
|
|
646
|
+
})
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return item
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Bind component-level props to a cloned node (handles flexStyle etc.)
|
|
654
|
+
_bindComponentPropsToNode(node, props, data, additionalScope = {}) {
|
|
655
|
+
if (!props || !data) return
|
|
656
|
+
|
|
657
|
+
const combinedData = { ...additionalScope, ...data }
|
|
658
|
+
|
|
659
|
+
Object.keys(props).forEach((key) => {
|
|
660
|
+
const value = props[key]
|
|
661
|
+
|
|
662
|
+
if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
|
|
663
|
+
let resolvedValue = extractTemplateValue(value, combinedData)
|
|
664
|
+
const keyLower = key.toLowerCase()
|
|
665
|
+
|
|
666
|
+
// Handle list/listData specially - try item data first, then additional scope
|
|
667
|
+
if (keyLower === 'list' || keyLower === 'listdata') {
|
|
668
|
+
console.log(
|
|
669
|
+
'[QtFastListView] Processing list prop, value:',
|
|
670
|
+
value,
|
|
671
|
+
'resolvedValue from itemData:',
|
|
672
|
+
resolvedValue
|
|
673
|
+
)
|
|
674
|
+
if (resolvedValue === undefined || !Array.isArray(resolvedValue)) {
|
|
675
|
+
resolvedValue = extractTemplateValue(value, combinedData)
|
|
676
|
+
console.log('[QtFastListView] resolvedValue from combinedData:', resolvedValue)
|
|
677
|
+
}
|
|
678
|
+
if (Array.isArray(resolvedValue)) {
|
|
679
|
+
// Find the component registered for this node and call setListData directly
|
|
680
|
+
const componentRegistry = window.__HIPPY_COMPONENT_REGISTRY__
|
|
681
|
+
console.log(
|
|
682
|
+
'[QtFastListView] Looking for component, registry size:',
|
|
683
|
+
componentRegistry?.size
|
|
684
|
+
)
|
|
685
|
+
if (componentRegistry) {
|
|
686
|
+
const component = componentRegistry.get(node)
|
|
687
|
+
console.log('[QtFastListView] Found component by node:', component?.constructor?.name)
|
|
688
|
+
if (component && typeof component.setListData === 'function') {
|
|
689
|
+
console.log(
|
|
690
|
+
'[QtFastListView] Calling setListData on component with',
|
|
691
|
+
resolvedValue.length,
|
|
692
|
+
'items'
|
|
693
|
+
)
|
|
694
|
+
component.setListData(resolvedValue)
|
|
695
|
+
} else {
|
|
696
|
+
// Fallback: store on node for later lookup
|
|
697
|
+
node._qtListData = resolvedValue
|
|
698
|
+
console.log('[QtFastListView] Set node._qtListData as fallback')
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
node._qtListData = resolvedValue
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return // Don't process further
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Handle flexStyle specially
|
|
708
|
+
if (
|
|
709
|
+
(keyLower === 'flexstyle' || keyLower === 'flexstyletemplate') &&
|
|
710
|
+
resolvedValue &&
|
|
711
|
+
typeof resolvedValue === 'object'
|
|
712
|
+
) {
|
|
713
|
+
const componentRegistry = window.__HIPPY_COMPONENT_REGISTRY__
|
|
714
|
+
const component = componentRegistry ? componentRegistry.get(node) : null
|
|
715
|
+
if (component && typeof component.updateProperty === 'function') {
|
|
716
|
+
component.updateProperty(key, resolvedValue)
|
|
717
|
+
} else {
|
|
718
|
+
this._applyFlexStyleToElement(node, resolvedValue)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Handle textSize and fontSize specially
|
|
723
|
+
if ((keyLower === 'textsize' || keyLower === 'fontsize') && resolvedValue !== undefined) {
|
|
724
|
+
const componentRegistry = window.__HIPPY_COMPONENT_REGISTRY__
|
|
725
|
+
const component = componentRegistry ? componentRegistry.get(node) : null
|
|
726
|
+
if (component && typeof component.updateProperty === 'function') {
|
|
727
|
+
component.updateProperty(key, resolvedValue)
|
|
728
|
+
} else {
|
|
729
|
+
const fontSize =
|
|
730
|
+
typeof resolvedValue === 'number' ? resolvedValue + 'px' : resolvedValue
|
|
731
|
+
node.style.fontSize = fontSize
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Handle src attribute
|
|
736
|
+
if (keyLower === 'src') {
|
|
737
|
+
if (resolvedValue && typeof resolvedValue === 'object') {
|
|
738
|
+
if (resolvedValue.url !== undefined) {
|
|
739
|
+
resolvedValue = resolvedValue.url
|
|
740
|
+
} else if (resolvedValue.uri !== undefined) {
|
|
741
|
+
resolvedValue = resolvedValue.uri
|
|
742
|
+
} else if (resolvedValue.src !== undefined) {
|
|
743
|
+
resolvedValue = resolvedValue.src
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
const componentRegistry = window.__HIPPY_COMPONENT_REGISTRY__
|
|
747
|
+
const component = componentRegistry ? componentRegistry.get(node) : null
|
|
748
|
+
if (component && typeof component.updateProperty === 'function') {
|
|
749
|
+
component.updateProperty(key, resolvedValue)
|
|
750
|
+
} else if (node.tagName === 'IMG' && typeof resolvedValue === 'string') {
|
|
751
|
+
node.src = resolvedValue
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
})
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Bind data to a node: replace ${prop} in text and attributes
|
|
759
|
+
_bindDataToNode(node, data, additionalScope = {}) {
|
|
760
|
+
bindTemplateDataToNode(node, data, {
|
|
761
|
+
scope: additionalScope,
|
|
762
|
+
onListData: (element, listValue) => {
|
|
763
|
+
element._qtListData = listValue
|
|
764
|
+
element.setAttribute('data-has-list', 'true')
|
|
765
|
+
},
|
|
766
|
+
})
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Replace ${prop} placeholders with actual values
|
|
770
|
+
_replacePlaceholders(str, data) {
|
|
771
|
+
return replaceTemplatePlaceholders(str, data)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Process child components that have list data
|
|
775
|
+
_processChildListData(container) {
|
|
776
|
+
// Find child nodes with _qtListData property
|
|
777
|
+
const processNode = (element) => {
|
|
778
|
+
// console.log(
|
|
779
|
+
// '[QtFastListView] _processChildListData checking element:',
|
|
780
|
+
// element.tagName,
|
|
781
|
+
// 'has _qtListData:',
|
|
782
|
+
// !!element._qtListData
|
|
783
|
+
// )
|
|
784
|
+
if (element._qtListData) {
|
|
785
|
+
// Find the component for this element
|
|
786
|
+
const componentRegistry = window.__HIPPY_COMPONENT_REGISTRY__
|
|
787
|
+
// console.log(
|
|
788
|
+
// '[QtFastListView] Found _qtListData:',
|
|
789
|
+
// element._qtListData.length,
|
|
790
|
+
// 'items, componentRegistry size:',
|
|
791
|
+
// componentRegistry?.size
|
|
792
|
+
// )
|
|
793
|
+
if (componentRegistry) {
|
|
794
|
+
const component = componentRegistry.get(element)
|
|
795
|
+
// console.log('[QtFastListView] Found component:', component?.constructor?.name)
|
|
796
|
+
if (component && typeof component.setListData === 'function') {
|
|
797
|
+
component.setListData(element._qtListData)
|
|
798
|
+
} else {
|
|
799
|
+
// DOM-only fallback: when templates are cloned as DOM, nested components are not instantiated.
|
|
800
|
+
// Handle tv-flex/FastFlexView by rendering in-place from its hidden templates.
|
|
801
|
+
const componentName = element.getAttribute?.('data-component-name')
|
|
802
|
+
const hasListTemplates = !!element.querySelector?.(
|
|
803
|
+
'[type], [data-template-type], [data-template], [data-singleton]'
|
|
804
|
+
)
|
|
805
|
+
if (componentName === 'QtFlexView') {
|
|
806
|
+
renderQtFlexViewInPlace(element, element._qtListData)
|
|
807
|
+
} else if (
|
|
808
|
+
componentName === 'FastListView' ||
|
|
809
|
+
componentName === 'ListView' ||
|
|
810
|
+
(element.hasAttribute?.('data-has-list') && hasListTemplates)
|
|
811
|
+
) {
|
|
812
|
+
renderQtFastListViewInPlace(element, element._qtListData)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
delete element._qtListData
|
|
817
|
+
}
|
|
818
|
+
// Recursively process children
|
|
819
|
+
if (element.children) {
|
|
820
|
+
Array.from(element.children).forEach((child) => processNode(child))
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
processNode(container)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Dispatch item click event
|
|
827
|
+
_findNestedItemMeta(target, stopElement) {
|
|
828
|
+
let el = target
|
|
829
|
+
if (el && el.nodeType === Node.TEXT_NODE) {
|
|
830
|
+
el = el.parentElement
|
|
831
|
+
}
|
|
832
|
+
while (el && el !== stopElement) {
|
|
833
|
+
if (el._itemData !== undefined && el._itemPosition !== undefined) {
|
|
834
|
+
return {
|
|
835
|
+
position: el._itemPosition,
|
|
836
|
+
itemData: el._itemData,
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
el = el.parentElement
|
|
840
|
+
}
|
|
841
|
+
return null
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
_dispatchItemClick(position, itemData, parentPosition) {
|
|
845
|
+
const payload = {
|
|
846
|
+
position: position,
|
|
847
|
+
index: position,
|
|
848
|
+
item: itemData,
|
|
849
|
+
}
|
|
850
|
+
if (parentPosition !== undefined && parentPosition !== null) {
|
|
851
|
+
payload.parentPosition = parentPosition
|
|
852
|
+
}
|
|
853
|
+
this.dispatchEvent('onItemclick', payload)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
_dispatchItemFocused(position, itemData, hasFocus) {
|
|
857
|
+
const payload = {
|
|
858
|
+
position: position,
|
|
859
|
+
index: position,
|
|
860
|
+
item: itemData,
|
|
861
|
+
hasFocus: !!hasFocus,
|
|
862
|
+
isFocused: !!hasFocus,
|
|
863
|
+
}
|
|
864
|
+
this.dispatchEvent('onItemfocused', payload)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
notifyItemFocusChanged(currentElement, hasFocus) {
|
|
868
|
+
if (!currentElement || !this._itemContainer) return
|
|
869
|
+
const items = this._itemContainer.children
|
|
870
|
+
const itemCount = items.length
|
|
871
|
+
if (itemCount === 0) return
|
|
872
|
+
|
|
873
|
+
let currentIndex = -1
|
|
874
|
+
let parentItem = null
|
|
875
|
+
for (let i = 0; i < itemCount; i++) {
|
|
876
|
+
if (items[i] === currentElement || items[i].contains(currentElement)) {
|
|
877
|
+
currentIndex = i
|
|
878
|
+
parentItem = items[i]
|
|
879
|
+
break
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (currentIndex === -1) return
|
|
883
|
+
|
|
884
|
+
const itemData = this._listData[currentIndex]
|
|
885
|
+
|
|
886
|
+
// Check if focus is on a nested item (waterfall -> section -> item)
|
|
887
|
+
if (
|
|
888
|
+
parentItem &&
|
|
889
|
+
itemData &&
|
|
890
|
+
typeof itemData === 'object' &&
|
|
891
|
+
Array.isArray(itemData.itemList)
|
|
892
|
+
) {
|
|
893
|
+
const nested = this._findNestedItemMeta(currentElement, parentItem)
|
|
894
|
+
if (nested && nested.itemData) {
|
|
895
|
+
// Dispatch focus event for nested item with parent position
|
|
896
|
+
const payload = {
|
|
897
|
+
position: nested.position,
|
|
898
|
+
index: nested.position,
|
|
899
|
+
item: nested.itemData,
|
|
900
|
+
hasFocus: !!hasFocus,
|
|
901
|
+
isFocused: !!hasFocus,
|
|
902
|
+
parentPosition: currentIndex,
|
|
903
|
+
}
|
|
904
|
+
this.dispatchEvent('onItemfocused', payload)
|
|
905
|
+
return
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (hasFocus) {
|
|
910
|
+
if (this._focusedIndex === currentIndex) return
|
|
911
|
+
this._focusedIndex = currentIndex
|
|
912
|
+
} else {
|
|
913
|
+
if (this._focusedIndex !== currentIndex) return
|
|
914
|
+
this._focusedIndex = -1
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
this._dispatchItemFocused(currentIndex, itemData, hasFocus)
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Add more items to the list
|
|
921
|
+
addListData(data) {
|
|
922
|
+
if (!Array.isArray(data)) return
|
|
923
|
+
const startIndex = this._listData.length
|
|
924
|
+
this._listData = this._listData.concat(data)
|
|
925
|
+
data.forEach((itemData, i) => {
|
|
926
|
+
const index = startIndex + i
|
|
927
|
+
const itemElement = this._createItemFromData(itemData, index)
|
|
928
|
+
if (itemElement) {
|
|
929
|
+
this._itemContainer.appendChild(itemElement)
|
|
930
|
+
}
|
|
931
|
+
})
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Update a single item
|
|
935
|
+
updateItem(position, data) {
|
|
936
|
+
if (position < 0 || position >= this._listData.length) return
|
|
937
|
+
this._listData[position] = { ...this._listData[position], ...data }
|
|
938
|
+
|
|
939
|
+
const existingItem = this._itemContainer.children[position]
|
|
940
|
+
if (existingItem) {
|
|
941
|
+
const newItem = this._createItemFromData(this._listData[position], position)
|
|
942
|
+
this._itemContainer.replaceChild(newItem, existingItem)
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Delete items
|
|
947
|
+
deleteItem(position, count = 1) {
|
|
948
|
+
this._listData.splice(position, count)
|
|
949
|
+
for (let i = 0; i < count; i++) {
|
|
950
|
+
const item = this._itemContainer.children[position]
|
|
951
|
+
if (item) {
|
|
952
|
+
this._itemContainer.removeChild(item)
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
Array.from(this._itemContainer.children).forEach((item, idx) => {
|
|
956
|
+
item.setAttribute('data-index', idx)
|
|
957
|
+
item.setAttribute('data-position', idx)
|
|
958
|
+
item._itemPosition = idx
|
|
959
|
+
})
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Clear all data
|
|
963
|
+
clearData() {
|
|
964
|
+
this._listData = []
|
|
965
|
+
this._itemContainer.innerHTML = ''
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Scroll to position
|
|
969
|
+
scrollToPosition(position, offset) {
|
|
970
|
+
const item = this._itemContainer.children[position]
|
|
971
|
+
if (item) {
|
|
972
|
+
item.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Request focus on item
|
|
977
|
+
requestChildFocus(position) {
|
|
978
|
+
const item = this._itemContainer.children[position]
|
|
979
|
+
if (item) {
|
|
980
|
+
item.focus()
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Set span count for grid layout
|
|
985
|
+
setSpanCount(count) {
|
|
986
|
+
if (count > 0) {
|
|
987
|
+
this._itemContainer.style.setProperty('--span-count', count)
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Cleanup when component is destroyed
|
|
992
|
+
destroy() {
|
|
993
|
+
// Unregister from FastListModule updates
|
|
994
|
+
unregisterFastListInstance(this)
|
|
995
|
+
// Call parent destroy if exists
|
|
996
|
+
if (super.destroy) {
|
|
997
|
+
super.destroy()
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* 处理焦点导航 - 根据布局类型计算下一个焦点元素
|
|
1003
|
+
* @param {HTMLElement} currentElement 当前焦点元素
|
|
1004
|
+
* @param {string} direction 方向: 'up' | 'down' | 'left' | 'right'
|
|
1005
|
+
* @returns {HTMLElement|null} 下一个焦点元素,如果没有则返回 null
|
|
1006
|
+
*/
|
|
1007
|
+
handleFocusNavigation(currentElement, direction) {
|
|
1008
|
+
console.log('[QtFastListView] handleFocusNavigation called:', { direction, currentElement })
|
|
1009
|
+
|
|
1010
|
+
const items = this._itemContainer.children
|
|
1011
|
+
const itemCount = items.length
|
|
1012
|
+
console.log('[QtFastListView] items:', { itemCount })
|
|
1013
|
+
|
|
1014
|
+
if (itemCount === 0) return null
|
|
1015
|
+
|
|
1016
|
+
// 找到当前焦点元素所在的 item
|
|
1017
|
+
let currentItem = null
|
|
1018
|
+
let currentIndex = -1
|
|
1019
|
+
for (let i = 0; i < itemCount; i++) {
|
|
1020
|
+
if (items[i] === currentElement || items[i].contains(currentElement)) {
|
|
1021
|
+
currentItem = items[i]
|
|
1022
|
+
currentIndex = i
|
|
1023
|
+
break
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
console.log('[QtFastListView] currentIndex:', currentIndex)
|
|
1028
|
+
|
|
1029
|
+
if (currentIndex === -1) return null
|
|
1030
|
+
|
|
1031
|
+
// 尝试在当前 item 内部进行焦点导航
|
|
1032
|
+
const internalNext = this._handleInternalNavigation(currentItem, currentElement, direction)
|
|
1033
|
+
console.log('[QtFastListView] internalNext:', !!internalNext)
|
|
1034
|
+
|
|
1035
|
+
if (internalNext) {
|
|
1036
|
+
return internalNext
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// 内部导航失败,检查是否在数据边界
|
|
1040
|
+
// 在第一个 item 且向上/左移动,滚动到头部
|
|
1041
|
+
if (currentIndex === 0 && (direction === 'up' || direction === 'left')) {
|
|
1042
|
+
console.log('[QtFastListView] 在第一个 item,滚动到头部')
|
|
1043
|
+
this._scrollToBoundary(direction, currentElement)
|
|
1044
|
+
}
|
|
1045
|
+
// 在最后一个 item 且向下/右移动,滚动到尾部
|
|
1046
|
+
else if (currentIndex === itemCount - 1 && (direction === 'down' || direction === 'right')) {
|
|
1047
|
+
console.log('[QtFastListView] 在最后一个 item,滚动到尾部')
|
|
1048
|
+
this._scrollToBoundary(direction, currentElement)
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// 尝试导航到其他 item
|
|
1052
|
+
let nextElement = null
|
|
1053
|
+
let nextIndex = -1
|
|
1054
|
+
|
|
1055
|
+
// 默认使用空间距离导航,支持瀑布流和复杂布局
|
|
1056
|
+
const result = this._handleSpatialNavigation(currentElement, direction, items, itemCount)
|
|
1057
|
+
if (result.element) {
|
|
1058
|
+
nextElement = result.element
|
|
1059
|
+
nextIndex = result.index
|
|
1060
|
+
} else {
|
|
1061
|
+
// 空间导航没找到时,作为后备回退到网格/列表基础逻辑
|
|
1062
|
+
if (this._spanCount > 0) {
|
|
1063
|
+
const fallback = this._handleGridNavigation(currentIndex, direction, items, itemCount)
|
|
1064
|
+
nextElement = fallback.element
|
|
1065
|
+
nextIndex = fallback.index
|
|
1066
|
+
} else {
|
|
1067
|
+
const fallback = this._handleListNavigation(currentIndex, direction, items, itemCount)
|
|
1068
|
+
nextElement = fallback.element
|
|
1069
|
+
nextIndex = fallback.index
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
console.log('[QtFastListView] navigation result:', { nextIndex, hasElement: !!nextElement })
|
|
1074
|
+
|
|
1075
|
+
return nextElement
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
updateBlockFocusDirections(currentElement) {
|
|
1079
|
+
if (!currentElement || !this._itemContainer) return
|
|
1080
|
+
|
|
1081
|
+
if (
|
|
1082
|
+
currentElement.hasAttribute &&
|
|
1083
|
+
(currentElement.hasAttribute('blockFocusDirections') ||
|
|
1084
|
+
currentElement.hasAttribute('blockfocusdirections'))
|
|
1085
|
+
) {
|
|
1086
|
+
return
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const items = this._itemContainer.children
|
|
1090
|
+
const itemCount = items.length
|
|
1091
|
+
if (itemCount === 0) return
|
|
1092
|
+
|
|
1093
|
+
let currentIndex = -1
|
|
1094
|
+
for (let i = 0; i < itemCount; i++) {
|
|
1095
|
+
if (items[i] === currentElement || items[i].contains(currentElement)) {
|
|
1096
|
+
currentIndex = i
|
|
1097
|
+
break
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (currentIndex === -1) return
|
|
1101
|
+
|
|
1102
|
+
// 默认不应该阻止焦点移出列表,这会导致无法导航到 Tabs 等外部组件
|
|
1103
|
+
// 只有在组件自身设置了 blockFocusDirections 时才生效
|
|
1104
|
+
// const blocked = []
|
|
1105
|
+
// if (this._horizontal) {
|
|
1106
|
+
// if (currentIndex === 0) blocked.push('left')
|
|
1107
|
+
// if (currentIndex === itemCount - 1) blocked.push('right')
|
|
1108
|
+
// } else {
|
|
1109
|
+
// if (currentIndex === 0) blocked.push('up')
|
|
1110
|
+
// if (currentIndex === itemCount - 1) blocked.push('down')
|
|
1111
|
+
// }
|
|
1112
|
+
|
|
1113
|
+
// if (blocked.length > 0) {
|
|
1114
|
+
// currentElement.setAttribute('blockfocusdirections', JSON.stringify(blocked))
|
|
1115
|
+
// } else {
|
|
1116
|
+
// currentElement.removeAttribute('blockfocusdirections')
|
|
1117
|
+
// currentElement.removeAttribute('blockFocusDirections')
|
|
1118
|
+
// }
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* 滚动到列表边界(头部或尾部)
|
|
1123
|
+
* @param {string} direction - 'left', 'right', 'up', 'down'
|
|
1124
|
+
* @param {HTMLElement} currentElement - 当前焦点的元素
|
|
1125
|
+
*/
|
|
1126
|
+
_scrollToBoundary(direction, currentElement) {
|
|
1127
|
+
if (!this.dom) return
|
|
1128
|
+
|
|
1129
|
+
// 为了实现嵌套列表(如 Waterfall -> Section -> Item)的精确边界滚动
|
|
1130
|
+
// 我们需要判断当前元素是否“真的”在这个方向的逻辑边缘上
|
|
1131
|
+
// 如果它不是在边缘,说明只是局部移动被拦截,不应该触发整个外层列表的回滚
|
|
1132
|
+
|
|
1133
|
+
// 获取当前列表所有直接的项
|
|
1134
|
+
const items = this._itemContainer ? Array.from(this._itemContainer.children) : []
|
|
1135
|
+
if (items.length === 0 || !currentElement) return
|
|
1136
|
+
|
|
1137
|
+
// 找到当前焦点元素所属的顶层项 (item)
|
|
1138
|
+
let currentItem = null
|
|
1139
|
+
let currentIndex = -1
|
|
1140
|
+
for (let i = 0; i < items.length; i++) {
|
|
1141
|
+
if (items[i] === currentElement || items[i].contains(currentElement)) {
|
|
1142
|
+
currentItem = items[i]
|
|
1143
|
+
currentIndex = i
|
|
1144
|
+
break
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (currentIndex === -1) return
|
|
1149
|
+
|
|
1150
|
+
// 获取所有可见项的坐标,用于判断是否是第一行/最后一行
|
|
1151
|
+
const itemRects = items
|
|
1152
|
+
.map((item) => ({
|
|
1153
|
+
el: item,
|
|
1154
|
+
rect: item.getBoundingClientRect(),
|
|
1155
|
+
}))
|
|
1156
|
+
.filter((info) => info.rect.width > 0 && info.rect.height > 0)
|
|
1157
|
+
|
|
1158
|
+
if (itemRects.length === 0) return
|
|
1159
|
+
|
|
1160
|
+
const currentRect = currentItem.getBoundingClientRect()
|
|
1161
|
+
|
|
1162
|
+
// 判断是否处于该方向的“最前排”
|
|
1163
|
+
let isAtBoundary = false
|
|
1164
|
+
const tolerance = 20 // 20px的行高容差
|
|
1165
|
+
|
|
1166
|
+
if (this._horizontal) {
|
|
1167
|
+
if (direction === 'left') {
|
|
1168
|
+
// 判断是否是最左边的一列(没有比它明显更靠左的元素)
|
|
1169
|
+
const minLeft = Math.min(...itemRects.map((i) => i.rect.left))
|
|
1170
|
+
isAtBoundary = currentRect.left <= minLeft + tolerance
|
|
1171
|
+
if (isAtBoundary) {
|
|
1172
|
+
// 这里必须直接设置 scrollTop/scrollLeft 为 0,而不是调用 scrollTo,
|
|
1173
|
+
// 因为 scrollTo 可能受到 CSS scroll-behavior 或其他库的干扰,导致滚动不到位。
|
|
1174
|
+
// 对于“回滚到边缘”的操作,直接重置是最稳妥的。
|
|
1175
|
+
this.dom.scrollLeft = 0
|
|
1176
|
+
}
|
|
1177
|
+
} else if (direction === 'right') {
|
|
1178
|
+
// 判断是否是最右边的一列
|
|
1179
|
+
const maxRight = Math.max(...itemRects.map((i) => i.rect.right))
|
|
1180
|
+
isAtBoundary = currentRect.right >= maxRight - tolerance
|
|
1181
|
+
if (isAtBoundary) {
|
|
1182
|
+
this.dom.scrollLeft = this.dom.scrollWidth
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
} else {
|
|
1186
|
+
if (direction === 'up') {
|
|
1187
|
+
// 判断是否是最高的一行(第一行)
|
|
1188
|
+
const minTop = Math.min(...itemRects.map((i) => i.rect.top))
|
|
1189
|
+
isAtBoundary = currentRect.top <= minTop + tolerance
|
|
1190
|
+
if (isAtBoundary) {
|
|
1191
|
+
// console.log(`[QtFastListView] Scroll to top triggered for ${this.dom.id}`);
|
|
1192
|
+
this.dom.scrollTop = 0
|
|
1193
|
+
// 如果外层还有滚动容器(比如页面本身的 body),也尝试重置一下,防止因为局部滚动导致的视差
|
|
1194
|
+
if (this.dom.parentElement && this.dom.parentElement.scrollTop > 0) {
|
|
1195
|
+
this.dom.parentElement.scrollTop = 0
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
} else if (direction === 'down') {
|
|
1199
|
+
// 判断是否是最底的一行(最后一行)
|
|
1200
|
+
const maxBottom = Math.max(...itemRects.map((i) => i.rect.bottom))
|
|
1201
|
+
isAtBoundary = currentRect.bottom >= maxBottom - tolerance
|
|
1202
|
+
if (isAtBoundary) {
|
|
1203
|
+
this.dom.scrollTop = this.dom.scrollHeight
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* 处理 item 内部的焦点导航
|
|
1211
|
+
* 当 item 内部有多个可聚焦元素时,先在内部导航
|
|
1212
|
+
*/
|
|
1213
|
+
_handleInternalNavigation(item, currentElement, direction) {
|
|
1214
|
+
// 查找 item 内所有可聚焦元素
|
|
1215
|
+
const focusables = item.querySelectorAll('[focusable="true"]')
|
|
1216
|
+
if (focusables.length <= 1) {
|
|
1217
|
+
return null // 只有一个或没有可聚焦元素,无法内部导航
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// 找到当前焦点在可聚焦元素列表中的位置
|
|
1221
|
+
let currentFocusIndex = -1
|
|
1222
|
+
for (let i = 0; i < focusables.length; i++) {
|
|
1223
|
+
if (focusables[i] === currentElement || focusables[i].contains(currentElement)) {
|
|
1224
|
+
currentFocusIndex = i
|
|
1225
|
+
break
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (currentFocusIndex === -1) {
|
|
1230
|
+
return null
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// 根据方向计算下一个焦点
|
|
1234
|
+
// 获取可聚焦元素的布局信息
|
|
1235
|
+
const focusableRects = Array.from(focusables).map((el) => ({
|
|
1236
|
+
el,
|
|
1237
|
+
rect: el.getBoundingClientRect(),
|
|
1238
|
+
}))
|
|
1239
|
+
|
|
1240
|
+
const currentRect = focusableRects[currentFocusIndex].rect
|
|
1241
|
+
let nextElement = null
|
|
1242
|
+
let minDistance = Infinity
|
|
1243
|
+
|
|
1244
|
+
focusableRects.forEach((item, index) => {
|
|
1245
|
+
if (index === currentFocusIndex) return
|
|
1246
|
+
|
|
1247
|
+
const rect = item.rect
|
|
1248
|
+
let distance = Infinity
|
|
1249
|
+
let isValidDirection = false
|
|
1250
|
+
|
|
1251
|
+
switch (direction) {
|
|
1252
|
+
case 'up':
|
|
1253
|
+
// 目标元素在当前元素上方
|
|
1254
|
+
if (rect.bottom <= currentRect.top) {
|
|
1255
|
+
distance = Math.abs(rect.left - currentRect.left) + (currentRect.top - rect.bottom)
|
|
1256
|
+
isValidDirection = true
|
|
1257
|
+
}
|
|
1258
|
+
break
|
|
1259
|
+
case 'down':
|
|
1260
|
+
// 目标元素在当前元素下方
|
|
1261
|
+
if (rect.top >= currentRect.bottom) {
|
|
1262
|
+
distance = Math.abs(rect.left - currentRect.left) + (rect.top - currentRect.bottom)
|
|
1263
|
+
isValidDirection = true
|
|
1264
|
+
}
|
|
1265
|
+
break
|
|
1266
|
+
case 'left':
|
|
1267
|
+
// 目标元素在当前元素左边
|
|
1268
|
+
if (rect.right <= currentRect.left) {
|
|
1269
|
+
distance = Math.abs(rect.top - currentRect.top) + (currentRect.left - rect.right)
|
|
1270
|
+
isValidDirection = true
|
|
1271
|
+
}
|
|
1272
|
+
break
|
|
1273
|
+
case 'right':
|
|
1274
|
+
// 目标元素在当前元素右边
|
|
1275
|
+
if (rect.left >= currentRect.right) {
|
|
1276
|
+
distance = Math.abs(rect.top - currentRect.top) + (rect.left - currentRect.right)
|
|
1277
|
+
isValidDirection = true
|
|
1278
|
+
}
|
|
1279
|
+
break
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if (isValidDirection && distance < minDistance) {
|
|
1283
|
+
minDistance = distance
|
|
1284
|
+
nextElement = item.el
|
|
1285
|
+
}
|
|
1286
|
+
})
|
|
1287
|
+
|
|
1288
|
+
return nextElement
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* 获取 item 内实际可聚焦的元素
|
|
1293
|
+
* @param {HTMLElement} item - 目标 item
|
|
1294
|
+
* @param {string} direction - 导航方向 ('up'|'down'|'left'|'right'),用于选择最近的焦点
|
|
1295
|
+
*/
|
|
1296
|
+
_getFocusableInItem(item, direction = null) {
|
|
1297
|
+
// 如果 item 本身可聚焦
|
|
1298
|
+
if (item.getAttribute('focusable') === 'true') {
|
|
1299
|
+
return item
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// 查找子元素中的所有可聚焦元素
|
|
1303
|
+
const focusables = item.querySelectorAll('[focusable="true"]')
|
|
1304
|
+
if (focusables.length === 0) {
|
|
1305
|
+
// 没有找到可聚焦元素,返回 item 本身
|
|
1306
|
+
return item
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if (focusables.length === 1 || !direction) {
|
|
1310
|
+
// 只有一个可聚焦元素,或没有方向信息,直接返回第一个
|
|
1311
|
+
return focusables[0]
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// 有多个可聚焦元素,根据方向选择最近的
|
|
1315
|
+
// 获取所有可聚焦元素的位置信息
|
|
1316
|
+
const focusableRects = Array.from(focusables).map((el) => ({
|
|
1317
|
+
el,
|
|
1318
|
+
rect: el.getBoundingClientRect(),
|
|
1319
|
+
}))
|
|
1320
|
+
|
|
1321
|
+
// 根据方向选择最佳元素
|
|
1322
|
+
// up: 选择最下方的元素(从下面进入)
|
|
1323
|
+
// down: 选择最上方的元素(从上面进入)
|
|
1324
|
+
// left: 选择最右边的元素(从右边进入)
|
|
1325
|
+
// right: 选择最左边的元素(从左边进入)
|
|
1326
|
+
let bestElement = focusableRects[0].el
|
|
1327
|
+
let bestValue = null
|
|
1328
|
+
|
|
1329
|
+
focusableRects.forEach((item) => {
|
|
1330
|
+
const rect = item.rect
|
|
1331
|
+
let value
|
|
1332
|
+
|
|
1333
|
+
switch (direction) {
|
|
1334
|
+
case 'up':
|
|
1335
|
+
// 从下往上导航,选择最下方的元素
|
|
1336
|
+
value = rect.bottom
|
|
1337
|
+
if (bestValue === null || value > bestValue) {
|
|
1338
|
+
bestValue = value
|
|
1339
|
+
bestElement = item.el
|
|
1340
|
+
}
|
|
1341
|
+
break
|
|
1342
|
+
case 'down':
|
|
1343
|
+
// 从上往下导航,选择最上方的元素
|
|
1344
|
+
value = rect.top
|
|
1345
|
+
if (bestValue === null || value < bestValue) {
|
|
1346
|
+
bestValue = value
|
|
1347
|
+
bestElement = item.el
|
|
1348
|
+
}
|
|
1349
|
+
break
|
|
1350
|
+
case 'left':
|
|
1351
|
+
// 从右往左导航,选择最右边的元素
|
|
1352
|
+
value = rect.right
|
|
1353
|
+
if (bestValue === null || value > bestValue) {
|
|
1354
|
+
bestValue = value
|
|
1355
|
+
bestElement = item.el
|
|
1356
|
+
}
|
|
1357
|
+
break
|
|
1358
|
+
case 'right':
|
|
1359
|
+
// 从左往右导航,选择最左边的元素
|
|
1360
|
+
value = rect.left
|
|
1361
|
+
if (bestValue === null || value < bestValue) {
|
|
1362
|
+
bestValue = value
|
|
1363
|
+
bestElement = item.el
|
|
1364
|
+
}
|
|
1365
|
+
break
|
|
1366
|
+
}
|
|
1367
|
+
})
|
|
1368
|
+
|
|
1369
|
+
return bestElement
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* 基于空间距离的焦点导航(适用于瀑布流或不规则布局)
|
|
1374
|
+
*/
|
|
1375
|
+
_handleSpatialNavigation(currentElement, direction, items, itemCount) {
|
|
1376
|
+
if (!currentElement) return { element: null, index: -1 }
|
|
1377
|
+
|
|
1378
|
+
const currentRect = currentElement.getBoundingClientRect()
|
|
1379
|
+
// 计算中心点坐标(相对于视口)
|
|
1380
|
+
const currentCenter = {
|
|
1381
|
+
x: currentRect.left + currentRect.width / 2,
|
|
1382
|
+
y: currentRect.top + currentRect.height / 2,
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
console.log(
|
|
1386
|
+
`[QtFastListView] SpatialNav start - Current Focus Center: (${currentCenter.x}, ${currentCenter.y}), direction: ${direction}`
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1389
|
+
let bestElement = null
|
|
1390
|
+
let bestIndex = -1
|
|
1391
|
+
let bestDistance = Infinity
|
|
1392
|
+
const crossWeight = 0.5 // 侧向距离权重
|
|
1393
|
+
|
|
1394
|
+
// 获取所有项的实际位置(处理绝对定位和相对定位)
|
|
1395
|
+
for (let i = 0; i < itemCount; i++) {
|
|
1396
|
+
const item = items[i]
|
|
1397
|
+
// 跳过不可见的项
|
|
1398
|
+
if (item.style.display === 'none') continue
|
|
1399
|
+
|
|
1400
|
+
const focusable = this._getFocusableInItem(item, direction)
|
|
1401
|
+
if (!focusable || focusable === currentElement) continue
|
|
1402
|
+
|
|
1403
|
+
// 如果整个 item 都不可聚焦(如瀑布流的空标题),跳过
|
|
1404
|
+
if (focusable === item && item.getAttribute('focusable') !== 'true') continue
|
|
1405
|
+
|
|
1406
|
+
// 必须使用 getBoundingClientRect 获取真实视口坐标,因为 waterfall 的子项可能是绝对定位的
|
|
1407
|
+
const rect = focusable.getBoundingClientRect()
|
|
1408
|
+
// 跳过尺寸为 0 的不可见元素
|
|
1409
|
+
if (rect.width === 0 && rect.height === 0) continue
|
|
1410
|
+
|
|
1411
|
+
const center = {
|
|
1412
|
+
x: rect.left + rect.width / 2,
|
|
1413
|
+
y: rect.top + rect.height / 2,
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
const dx = center.x - currentCenter.x
|
|
1417
|
+
const dy = center.y - currentCenter.y
|
|
1418
|
+
|
|
1419
|
+
let isInDirection = false
|
|
1420
|
+
let primaryDistance = 0
|
|
1421
|
+
let crossDistance = 0
|
|
1422
|
+
|
|
1423
|
+
switch (direction) {
|
|
1424
|
+
case 'up':
|
|
1425
|
+
isInDirection = dy < -5 // 减小容差,更精确匹配
|
|
1426
|
+
primaryDistance = Math.abs(dy)
|
|
1427
|
+
crossDistance = Math.abs(dx)
|
|
1428
|
+
break
|
|
1429
|
+
case 'down':
|
|
1430
|
+
isInDirection = dy > 5
|
|
1431
|
+
primaryDistance = Math.abs(dy)
|
|
1432
|
+
crossDistance = Math.abs(dx)
|
|
1433
|
+
break
|
|
1434
|
+
case 'left':
|
|
1435
|
+
isInDirection = dx < -5
|
|
1436
|
+
primaryDistance = Math.abs(dx)
|
|
1437
|
+
crossDistance = Math.abs(dy)
|
|
1438
|
+
break
|
|
1439
|
+
case 'right':
|
|
1440
|
+
isInDirection = dx > 5
|
|
1441
|
+
primaryDistance = Math.abs(dx)
|
|
1442
|
+
crossDistance = Math.abs(dy)
|
|
1443
|
+
break
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (isInDirection) {
|
|
1447
|
+
// 主方向距离 + 侧向距离加权
|
|
1448
|
+
// 对于左右移动,如果 y 轴差异很大(不在同一行),应大大增加距离惩罚
|
|
1449
|
+
if ((direction === 'left' || direction === 'right') && Math.abs(dy) > rect.height / 2) {
|
|
1450
|
+
crossDistance *= 5 // 如果不在同一行,极大地降低其被选中的概率
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// 对于上下移动,如果 x 轴差异很大(不在同一列),应大大增加距离惩罚
|
|
1454
|
+
if ((direction === 'up' || direction === 'down') && Math.abs(dx) > rect.width / 2) {
|
|
1455
|
+
crossDistance *= 5
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const distance = primaryDistance + crossDistance * crossWeight
|
|
1459
|
+
|
|
1460
|
+
console.log(
|
|
1461
|
+
`[QtFastListView] SpatialNav candidate - Index: ${i}, Center: (${center.x}, ${center.y}), dx: ${dx}, dy: ${dy}, distance: ${distance}`
|
|
1462
|
+
)
|
|
1463
|
+
|
|
1464
|
+
if (distance < bestDistance) {
|
|
1465
|
+
bestDistance = distance
|
|
1466
|
+
bestElement = focusable
|
|
1467
|
+
bestIndex = i
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
if (bestElement) {
|
|
1473
|
+
console.log(
|
|
1474
|
+
`[QtFastListView] SpatialNav success - Best Index: ${bestIndex}, distance: ${bestDistance}`
|
|
1475
|
+
)
|
|
1476
|
+
} else {
|
|
1477
|
+
console.log(
|
|
1478
|
+
`[QtFastListView] SpatialNav failed - No candidate found for direction ${direction}`
|
|
1479
|
+
)
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
return { element: bestElement, index: bestIndex }
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* 网格布局的焦点导航
|
|
1487
|
+
* @returns { element: HTMLElement|null, index: number }
|
|
1488
|
+
*/
|
|
1489
|
+
_handleGridNavigation(currentIndex, direction, items, itemCount) {
|
|
1490
|
+
const spanCount = this._spanCount
|
|
1491
|
+
let currentRow = Math.floor(currentIndex / spanCount)
|
|
1492
|
+
let currentCol = currentIndex % spanCount
|
|
1493
|
+
|
|
1494
|
+
let nextIndex = currentIndex
|
|
1495
|
+
|
|
1496
|
+
while (true) {
|
|
1497
|
+
switch (direction) {
|
|
1498
|
+
case 'up':
|
|
1499
|
+
if (currentRow > 0) {
|
|
1500
|
+
nextIndex -= spanCount
|
|
1501
|
+
currentRow--
|
|
1502
|
+
} else {
|
|
1503
|
+
nextIndex = -1
|
|
1504
|
+
}
|
|
1505
|
+
break
|
|
1506
|
+
case 'down':
|
|
1507
|
+
nextIndex += spanCount
|
|
1508
|
+
currentRow++
|
|
1509
|
+
if (nextIndex >= itemCount) {
|
|
1510
|
+
nextIndex = -1
|
|
1511
|
+
}
|
|
1512
|
+
break
|
|
1513
|
+
case 'left':
|
|
1514
|
+
if (currentCol > 0) {
|
|
1515
|
+
nextIndex -= 1
|
|
1516
|
+
currentCol--
|
|
1517
|
+
} else {
|
|
1518
|
+
nextIndex = -1
|
|
1519
|
+
}
|
|
1520
|
+
break
|
|
1521
|
+
case 'right':
|
|
1522
|
+
if (currentCol < spanCount - 1 && nextIndex + 1 < itemCount) {
|
|
1523
|
+
nextIndex += 1
|
|
1524
|
+
currentCol++
|
|
1525
|
+
} else {
|
|
1526
|
+
nextIndex = -1
|
|
1527
|
+
}
|
|
1528
|
+
break
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
if (nextIndex >= 0 && nextIndex < itemCount) {
|
|
1532
|
+
const focusable = this._getFocusableInItem(items[nextIndex], direction)
|
|
1533
|
+
if (
|
|
1534
|
+
focusable !== items[nextIndex] ||
|
|
1535
|
+
items[nextIndex].getAttribute('focusable') === 'true'
|
|
1536
|
+
) {
|
|
1537
|
+
return { element: focusable, index: nextIndex }
|
|
1538
|
+
}
|
|
1539
|
+
// 如果当前项不可聚焦,继续在同方向上查找
|
|
1540
|
+
continue
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
break
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
return { element: null, index: -1 }
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* 列表布局的焦点导航
|
|
1551
|
+
* @returns { element: HTMLElement|null, index: number }
|
|
1552
|
+
*/
|
|
1553
|
+
_handleListNavigation(currentIndex, direction, items, itemCount) {
|
|
1554
|
+
let nextIndex = currentIndex
|
|
1555
|
+
|
|
1556
|
+
while (true) {
|
|
1557
|
+
if (this._horizontal) {
|
|
1558
|
+
// 水平列表
|
|
1559
|
+
switch (direction) {
|
|
1560
|
+
case 'left':
|
|
1561
|
+
if (nextIndex > 0) {
|
|
1562
|
+
nextIndex -= 1
|
|
1563
|
+
} else {
|
|
1564
|
+
nextIndex = -1
|
|
1565
|
+
}
|
|
1566
|
+
break
|
|
1567
|
+
case 'right':
|
|
1568
|
+
if (nextIndex < itemCount - 1) {
|
|
1569
|
+
nextIndex += 1
|
|
1570
|
+
} else {
|
|
1571
|
+
nextIndex = -1
|
|
1572
|
+
}
|
|
1573
|
+
break
|
|
1574
|
+
default:
|
|
1575
|
+
nextIndex = -1
|
|
1576
|
+
break
|
|
1577
|
+
}
|
|
1578
|
+
} else {
|
|
1579
|
+
// 垂直列表
|
|
1580
|
+
switch (direction) {
|
|
1581
|
+
case 'up':
|
|
1582
|
+
if (nextIndex > 0) {
|
|
1583
|
+
nextIndex -= 1
|
|
1584
|
+
} else {
|
|
1585
|
+
nextIndex = -1
|
|
1586
|
+
}
|
|
1587
|
+
break
|
|
1588
|
+
case 'down':
|
|
1589
|
+
if (nextIndex < itemCount - 1) {
|
|
1590
|
+
nextIndex += 1
|
|
1591
|
+
} else {
|
|
1592
|
+
nextIndex = -1
|
|
1593
|
+
}
|
|
1594
|
+
break
|
|
1595
|
+
default:
|
|
1596
|
+
nextIndex = -1
|
|
1597
|
+
break
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
if (nextIndex >= 0 && nextIndex < itemCount) {
|
|
1602
|
+
const focusable = this._getFocusableInItem(items[nextIndex], direction)
|
|
1603
|
+
if (
|
|
1604
|
+
focusable !== items[nextIndex] ||
|
|
1605
|
+
items[nextIndex].getAttribute('focusable') === 'true'
|
|
1606
|
+
) {
|
|
1607
|
+
return { element: focusable, index: nextIndex }
|
|
1608
|
+
}
|
|
1609
|
+
// 如果当前项不可聚焦,继续在同方向上查找
|
|
1610
|
+
continue
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
break
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
return { element: null, index: -1 }
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// Extract the actual value from a ${prop} placeholder
|
|
1620
|
+
_extractValue(placeholder, data) {
|
|
1621
|
+
return extractTemplateValue(placeholder, data)
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Apply flexStyle object to a DOM element
|
|
1625
|
+
_applyFlexStyleToElement(element, styleObj) {
|
|
1626
|
+
if (!styleObj || typeof styleObj !== 'object') return
|
|
1627
|
+
|
|
1628
|
+
Object.keys(styleObj).forEach((key) => {
|
|
1629
|
+
let value = styleObj[key]
|
|
1630
|
+
|
|
1631
|
+
if (typeof value === 'number') {
|
|
1632
|
+
const needsPx = [
|
|
1633
|
+
'width',
|
|
1634
|
+
'height',
|
|
1635
|
+
'minWidth',
|
|
1636
|
+
'minHeight',
|
|
1637
|
+
'maxWidth',
|
|
1638
|
+
'maxHeight',
|
|
1639
|
+
'padding',
|
|
1640
|
+
'paddingTop',
|
|
1641
|
+
'paddingRight',
|
|
1642
|
+
'paddingBottom',
|
|
1643
|
+
'paddingLeft',
|
|
1644
|
+
'margin',
|
|
1645
|
+
'marginTop',
|
|
1646
|
+
'marginRight',
|
|
1647
|
+
'marginBottom',
|
|
1648
|
+
'marginLeft',
|
|
1649
|
+
'top',
|
|
1650
|
+
'left',
|
|
1651
|
+
'right',
|
|
1652
|
+
'bottom',
|
|
1653
|
+
'borderRadius',
|
|
1654
|
+
'borderWidth',
|
|
1655
|
+
'fontSize',
|
|
1656
|
+
'lineHeight',
|
|
1657
|
+
'letterSpacing',
|
|
1658
|
+
'gap',
|
|
1659
|
+
'rowGap',
|
|
1660
|
+
'columnGap',
|
|
1661
|
+
].includes(key)
|
|
1662
|
+
|
|
1663
|
+
if (needsPx) {
|
|
1664
|
+
value = value + 'px'
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
try {
|
|
1669
|
+
element.style[key] = value
|
|
1670
|
+
} catch (e) {
|
|
1671
|
+
try {
|
|
1672
|
+
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
|
|
1673
|
+
element.style.setProperty(cssKey, value)
|
|
1674
|
+
} catch (err) {
|
|
1675
|
+
// Ignore style errors
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
})
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// Override updateChildzIndex to safely handle null children
|
|
1682
|
+
updateChildzIndex() {
|
|
1683
|
+
if (!this.dom) return
|
|
1684
|
+
const uiManager = this.context.getModuleByName('UIManagerModule')
|
|
1685
|
+
this.dom.childNodes.forEach((item) => {
|
|
1686
|
+
const childDom = uiManager.findViewById(item.id)
|
|
1687
|
+
if (!childDom) return
|
|
1688
|
+
|
|
1689
|
+
if (this.exitChildrenStackContext && childDom.props?.style?.zIndex !== undefined) {
|
|
1690
|
+
childDom.updateSelfStackContext()
|
|
1691
|
+
}
|
|
1692
|
+
if (!this.exitChildrenStackContext && childDom.updatedZIndex) {
|
|
1693
|
+
childDom.updateSelfStackContext(false)
|
|
1694
|
+
}
|
|
1695
|
+
})
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function parseDomTemplateType(element) {
|
|
1700
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return null
|
|
1701
|
+
const raw = element.getAttribute('data-template-type') ?? element.getAttribute('type')
|
|
1702
|
+
if (raw === null || raw === undefined || raw === '') return null
|
|
1703
|
+
return isNaN(Number(raw)) ? raw : Number(raw)
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function collectDomTemplates(rootElement, itemContainer) {
|
|
1707
|
+
const templates = []
|
|
1708
|
+
const directChildren = Array.from(rootElement.children || []).filter(
|
|
1709
|
+
(child) => child !== itemContainer
|
|
1710
|
+
)
|
|
1711
|
+
|
|
1712
|
+
directChildren.forEach((element) => {
|
|
1713
|
+
const ownType = parseDomTemplateType(element)
|
|
1714
|
+
const isSingleton =
|
|
1715
|
+
element.hasAttribute?.('singleton') || element.hasAttribute?.('data-singleton')
|
|
1716
|
+
|
|
1717
|
+
if (ownType !== null || isSingleton) {
|
|
1718
|
+
element.setAttribute(isSingleton ? 'data-singleton' : 'data-template', 'true')
|
|
1719
|
+
if (ownType !== null) {
|
|
1720
|
+
element.setAttribute('data-template-type', String(ownType))
|
|
1721
|
+
}
|
|
1722
|
+
element.style.display = 'none'
|
|
1723
|
+
templates.push({ dom: element, type: ownType, singleton: isSingleton })
|
|
1724
|
+
return
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
const nestedTemplates = element.querySelectorAll
|
|
1728
|
+
? Array.from(
|
|
1729
|
+
element.querySelectorAll('[type], [data-template-type], [singleton], [data-singleton]')
|
|
1730
|
+
)
|
|
1731
|
+
: []
|
|
1732
|
+
|
|
1733
|
+
if (nestedTemplates.length > 0) {
|
|
1734
|
+
element.style.display = 'none'
|
|
1735
|
+
nestedTemplates.forEach((nested) => {
|
|
1736
|
+
const nestedType = parseDomTemplateType(nested)
|
|
1737
|
+
const nestedSingleton =
|
|
1738
|
+
nested.hasAttribute?.('singleton') || nested.hasAttribute?.('data-singleton')
|
|
1739
|
+
nested.setAttribute(nestedSingleton ? 'data-singleton' : 'data-template', 'true')
|
|
1740
|
+
if (nestedType !== null) {
|
|
1741
|
+
nested.setAttribute('data-template-type', String(nestedType))
|
|
1742
|
+
}
|
|
1743
|
+
nested.style.display = 'none'
|
|
1744
|
+
templates.push({ dom: nested, type: nestedType, singleton: nestedSingleton })
|
|
1745
|
+
})
|
|
1746
|
+
return
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
element.setAttribute('data-template', 'true')
|
|
1750
|
+
element.style.display = 'none'
|
|
1751
|
+
templates.push({ dom: element, type: null, singleton: false })
|
|
1752
|
+
})
|
|
1753
|
+
|
|
1754
|
+
return templates
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function configureDomListItemContainer(rootElement, itemContainer) {
|
|
1758
|
+
const isHorizontal =
|
|
1759
|
+
rootElement.hasAttribute('horizontal') || rootElement.getAttribute('horizontal') === ''
|
|
1760
|
+
|
|
1761
|
+
const spanCountValue =
|
|
1762
|
+
rootElement.getAttribute('spanCount') || rootElement.getAttribute('span-count') || '0'
|
|
1763
|
+
const spanCount = Number(spanCountValue) || 0
|
|
1764
|
+
|
|
1765
|
+
if (spanCount > 0) {
|
|
1766
|
+
itemContainer.style.display = 'grid'
|
|
1767
|
+
itemContainer.style.gridTemplateColumns = `repeat(${spanCount}, 1fr)`
|
|
1768
|
+
itemContainer.style.flexDirection = ''
|
|
1769
|
+
itemContainer.style.flexWrap = ''
|
|
1770
|
+
itemContainer.style.overflowX = 'hidden'
|
|
1771
|
+
itemContainer.style.overflowY = 'auto'
|
|
1772
|
+
return
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
itemContainer.style.display = 'flex'
|
|
1776
|
+
itemContainer.style.gridTemplateColumns = ''
|
|
1777
|
+
|
|
1778
|
+
if (isHorizontal) {
|
|
1779
|
+
itemContainer.style.flexDirection = 'row'
|
|
1780
|
+
itemContainer.style.flexWrap = 'nowrap'
|
|
1781
|
+
itemContainer.style.overflowX = 'auto'
|
|
1782
|
+
itemContainer.style.overflowY = 'hidden'
|
|
1783
|
+
return
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
itemContainer.style.flexDirection = 'column'
|
|
1787
|
+
itemContainer.style.flexWrap = 'nowrap'
|
|
1788
|
+
itemContainer.style.overflowX = 'hidden'
|
|
1789
|
+
itemContainer.style.overflowY = 'auto'
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function bindDomOnlyListNode(node, itemData, listData) {
|
|
1793
|
+
bindTemplateDataToNode(node, itemData, {
|
|
1794
|
+
scope: { itemList: listData },
|
|
1795
|
+
onListData: (element, value) => {
|
|
1796
|
+
element._qtListData = value
|
|
1797
|
+
element.setAttribute('data-has-list', 'true')
|
|
1798
|
+
},
|
|
1799
|
+
})
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
export function renderQtFastListViewInPlace(rootElement, listData) {
|
|
1803
|
+
if (!rootElement) return
|
|
1804
|
+
if (Array.isArray(listData) && listData.length === 1 && Array.isArray(listData[0])) {
|
|
1805
|
+
listData = listData[0]
|
|
1806
|
+
}
|
|
1807
|
+
if (!Array.isArray(listData)) return
|
|
1808
|
+
if (rootElement.style.display === 'none') {
|
|
1809
|
+
rootElement.style.display = ''
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
let itemContainer = Array.from(rootElement.children || []).find(
|
|
1813
|
+
(child) => child.classList && child.classList.contains('fast-list-item-container')
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
if (!itemContainer) {
|
|
1817
|
+
itemContainer = document.createElement('div')
|
|
1818
|
+
itemContainer.className = 'fast-list-item-container'
|
|
1819
|
+
rootElement.appendChild(itemContainer)
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
configureDomListItemContainer(rootElement, itemContainer)
|
|
1823
|
+
|
|
1824
|
+
const templates = collectDomTemplates(rootElement, itemContainer)
|
|
1825
|
+
const sharedTemplates = window.__QT_SHARED_TEMPLATES__
|
|
1826
|
+
itemContainer.innerHTML = ''
|
|
1827
|
+
|
|
1828
|
+
if (listData.length === 0) {
|
|
1829
|
+
return
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const renderedSingletonTypes = new Set()
|
|
1833
|
+
const singletonTemplates = templates.filter((template) => template.singleton)
|
|
1834
|
+
const regularTemplates = templates.filter((template) => !template.singleton)
|
|
1835
|
+
|
|
1836
|
+
singletonTemplates.forEach((template) => {
|
|
1837
|
+
let matchingData = null
|
|
1838
|
+
if (template.type !== null && template.type !== undefined) {
|
|
1839
|
+
matchingData = listData.find((item) => item?.type === template.type) || null
|
|
1840
|
+
} else {
|
|
1841
|
+
matchingData = listData[0] || null
|
|
1842
|
+
}
|
|
1843
|
+
if (!matchingData) {
|
|
1844
|
+
template.dom.style.display = 'none'
|
|
1845
|
+
return
|
|
1846
|
+
}
|
|
1847
|
+
template.dom.style.display = ''
|
|
1848
|
+
bindDomOnlyListNode(template.dom, matchingData, listData)
|
|
1849
|
+
applyDomOnlyTemplateRootDefaults(template.dom)
|
|
1850
|
+
itemContainer.appendChild(template.dom)
|
|
1851
|
+
if (matchingData.type !== null && matchingData.type !== undefined) {
|
|
1852
|
+
renderedSingletonTypes.add(matchingData.type)
|
|
1853
|
+
}
|
|
1854
|
+
})
|
|
1855
|
+
|
|
1856
|
+
const renderTemplates = regularTemplates.length > 0 ? regularTemplates : templates
|
|
1857
|
+
listData.forEach((itemData, index) => {
|
|
1858
|
+
if (renderedSingletonTypes.has(itemData?.type)) {
|
|
1859
|
+
return
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
let matchingTemplate = renderTemplates.find((template) => template.type === itemData?.type)
|
|
1863
|
+
if (
|
|
1864
|
+
!matchingTemplate &&
|
|
1865
|
+
sharedTemplates &&
|
|
1866
|
+
itemData?.type !== null &&
|
|
1867
|
+
itemData?.type !== undefined
|
|
1868
|
+
) {
|
|
1869
|
+
const sharedByType = sharedTemplates.get(itemData.type)
|
|
1870
|
+
if (sharedByType?.dom) {
|
|
1871
|
+
matchingTemplate = sharedByType
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
if (!matchingTemplate) {
|
|
1875
|
+
matchingTemplate = renderTemplates.find((template) => template.type === null)
|
|
1876
|
+
}
|
|
1877
|
+
if (!matchingTemplate) {
|
|
1878
|
+
if (sharedTemplates && sharedTemplates.size > 0) {
|
|
1879
|
+
const firstShared = sharedTemplates.values().next().value
|
|
1880
|
+
if (firstShared?.dom) {
|
|
1881
|
+
matchingTemplate = firstShared
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
if (!matchingTemplate) {
|
|
1886
|
+
matchingTemplate = renderTemplates[0]
|
|
1887
|
+
}
|
|
1888
|
+
if (!matchingTemplate?.dom) {
|
|
1889
|
+
return
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
const itemWrapper = document.createElement('div')
|
|
1893
|
+
itemWrapper.setAttribute('data-index', index)
|
|
1894
|
+
itemWrapper.setAttribute('data-position', index)
|
|
1895
|
+
itemWrapper._itemData = itemData
|
|
1896
|
+
itemWrapper._itemPosition = index
|
|
1897
|
+
|
|
1898
|
+
if (itemData && itemData.decoration && typeof itemData.decoration === 'object') {
|
|
1899
|
+
const decoration = itemData.decoration
|
|
1900
|
+
if (decoration.top !== undefined) itemWrapper.style.marginTop = decoration.top + 'px'
|
|
1901
|
+
if (decoration.left !== undefined) itemWrapper.style.marginLeft = decoration.left + 'px'
|
|
1902
|
+
if (decoration.right !== undefined) itemWrapper.style.marginRight = decoration.right + 'px'
|
|
1903
|
+
if (decoration.bottom !== undefined) itemWrapper.style.marginBottom = decoration.bottom + 'px'
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const clonedNode = matchingTemplate.dom.cloneNode(true)
|
|
1907
|
+
clonedNode.removeAttribute('data-template')
|
|
1908
|
+
clonedNode.removeAttribute('data-singleton')
|
|
1909
|
+
clonedNode.style.display = ''
|
|
1910
|
+
clonedNode.style.position = 'relative'
|
|
1911
|
+
|
|
1912
|
+
bindDomOnlyListNode(clonedNode, itemData, listData)
|
|
1913
|
+
applyDomOnlyTemplateRootDefaults(clonedNode)
|
|
1914
|
+
|
|
1915
|
+
itemWrapper.appendChild(clonedNode)
|
|
1916
|
+
itemContainer.appendChild(itemWrapper)
|
|
1917
|
+
})
|
|
1918
|
+
|
|
1919
|
+
requestAnimationFrame(() => syncDomAutoWidthIn(itemContainer))
|
|
1920
|
+
}
|