@quicktvui/web-renderer 1.0.0 → 1.0.2
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 +1 -1
- package/src/components/QtBaseComponent.js +11 -0
- package/src/components/TabsView.js +211 -8
- package/src/index.js +51 -0
package/package.json
CHANGED
|
@@ -62,6 +62,17 @@ export class QtBaseComponent extends HippyWebView {
|
|
|
62
62
|
}
|
|
63
63
|
return true
|
|
64
64
|
|
|
65
|
+
case 'name':
|
|
66
|
+
// 存储 name 属性到组件实例和 DOM 上,用于子组件查找
|
|
67
|
+
this.props = this.props || {}
|
|
68
|
+
this.props.name = value
|
|
69
|
+
if (value) {
|
|
70
|
+
this.dom.setAttribute('name', String(value))
|
|
71
|
+
} else {
|
|
72
|
+
this.dom.removeAttribute('name')
|
|
73
|
+
}
|
|
74
|
+
return true
|
|
75
|
+
|
|
65
76
|
default:
|
|
66
77
|
return false
|
|
67
78
|
}
|
|
@@ -3,6 +3,9 @@ import { QtBaseComponent } from './QtBaseComponent'
|
|
|
3
3
|
import { registerComponent } from '../core/componentRegistry'
|
|
4
4
|
|
|
5
5
|
export class TabsView extends QtBaseComponent {
|
|
6
|
+
// 静态变量追踪实例数量
|
|
7
|
+
static _instanceCount = 0
|
|
8
|
+
|
|
6
9
|
constructor(context, id, pId) {
|
|
7
10
|
super(context, id, pId)
|
|
8
11
|
this.tagName = 'TabsView'
|
|
@@ -10,12 +13,22 @@ export class TabsView extends QtBaseComponent {
|
|
|
10
13
|
this.dom.setAttribute('data-component-name', 'TabsView')
|
|
11
14
|
registerComponent(id, this)
|
|
12
15
|
|
|
16
|
+
// 追踪实例
|
|
17
|
+
this._instanceId = ++TabsView._instanceCount
|
|
18
|
+
console.log('[TabsView] constructor, instanceId:', this._instanceId)
|
|
19
|
+
|
|
13
20
|
// Tabs state
|
|
14
21
|
this._tabData = null
|
|
15
22
|
this._currentPageIndex = 0
|
|
16
23
|
this._pages = new Map()
|
|
17
24
|
this._tabItems = []
|
|
18
25
|
|
|
26
|
+
// 记录已加载过数据的页面,避免重复加载
|
|
27
|
+
this._loadedPageIndices = new Set()
|
|
28
|
+
|
|
29
|
+
// 记录最后一次分发 pageChanged 的页面索引,避免重复触发
|
|
30
|
+
this._lastPageChangedIndex = -1
|
|
31
|
+
|
|
19
32
|
// 子组件引用,通过 name 属性存储
|
|
20
33
|
// 'tabList' -> qt-nav-bar
|
|
21
34
|
// 'content' -> recycler-view-pager
|
|
@@ -70,11 +83,17 @@ export class TabsView extends QtBaseComponent {
|
|
|
70
83
|
console.log('[TabsView] intercepted event from tabList:', eventName, params)
|
|
71
84
|
|
|
72
85
|
// 处理 onItemFocused 事件 (hasFocus === true 时切换页面)
|
|
73
|
-
|
|
86
|
+
// 注意:事件名可能是 onItemfocused 或 onItemFocused
|
|
87
|
+
if (
|
|
88
|
+
(eventName === 'onItemFocused' || eventName === 'onItemfocused') &&
|
|
89
|
+
params &&
|
|
90
|
+
params.hasFocus === true
|
|
91
|
+
) {
|
|
74
92
|
const pageIndex = params.position
|
|
75
93
|
if (pageIndex !== undefined && pageIndex !== this._currentPageIndex) {
|
|
76
94
|
console.log('[TabsView] tab focus changed to:', pageIndex)
|
|
77
95
|
this._currentPageIndex = pageIndex
|
|
96
|
+
this._syncContentCurrentPage(pageIndex)
|
|
78
97
|
this._dispatchPageChanged(pageIndex)
|
|
79
98
|
this._dispatchLoadPageData(pageIndex, false)
|
|
80
99
|
}
|
|
@@ -86,15 +105,58 @@ export class TabsView extends QtBaseComponent {
|
|
|
86
105
|
if (pageIndex !== undefined && pageIndex !== this._currentPageIndex) {
|
|
87
106
|
console.log('[TabsView] tab clicked, switching to:', pageIndex)
|
|
88
107
|
this._currentPageIndex = pageIndex
|
|
108
|
+
this._syncContentCurrentPage(pageIndex)
|
|
89
109
|
this._dispatchPageChanged(pageIndex)
|
|
90
110
|
this._dispatchLoadPageData(pageIndex, false)
|
|
91
111
|
}
|
|
92
112
|
}
|
|
93
113
|
}
|
|
94
114
|
|
|
115
|
+
// 添加 DOM 焦点事件监听,用于捕获 tab 切换
|
|
116
|
+
this._boundHandleTabFocusIn = this._handleTabFocusIn.bind(this)
|
|
117
|
+
tabList.dom.addEventListener('focusin', this._boundHandleTabFocusIn)
|
|
118
|
+
|
|
95
119
|
console.log('[TabsView] tabList listeners setup complete')
|
|
96
120
|
}
|
|
97
121
|
|
|
122
|
+
// 处理 tab 导航栏中的 DOM 焦点事件
|
|
123
|
+
_handleTabFocusIn(event) {
|
|
124
|
+
const tabList = this.getChildByName('tabList')
|
|
125
|
+
if (!tabList) return
|
|
126
|
+
|
|
127
|
+
// 查找 itemContainer
|
|
128
|
+
let itemContainer = tabList._itemContainer
|
|
129
|
+
if (!itemContainer) {
|
|
130
|
+
itemContainer = tabList.dom.querySelector('.flex-view-item-container')
|
|
131
|
+
}
|
|
132
|
+
if (!itemContainer) return
|
|
133
|
+
|
|
134
|
+
// 找到焦点元素对应的 tab 索引
|
|
135
|
+
const items = itemContainer.children
|
|
136
|
+
let focusIndex = -1
|
|
137
|
+
for (let i = 0; i < items.length; i++) {
|
|
138
|
+
if (items[i] === event.target || items[i].contains(event.target)) {
|
|
139
|
+
focusIndex = i
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (focusIndex !== -1 && focusIndex !== this._currentPageIndex) {
|
|
145
|
+
console.log('[TabsView] DOM focus changed to tab:', focusIndex)
|
|
146
|
+
this._currentPageIndex = focusIndex
|
|
147
|
+
this._syncContentCurrentPage(focusIndex)
|
|
148
|
+
this._dispatchPageChanged(focusIndex)
|
|
149
|
+
this._dispatchLoadPageData(focusIndex, false)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_syncContentCurrentPage(pageIndex) {
|
|
154
|
+
const content = this.getChildByName('content')
|
|
155
|
+
if (content && typeof content.setCurrentPage === 'function') {
|
|
156
|
+
content.setCurrentPage(pageIndex)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
98
160
|
// Remove child
|
|
99
161
|
removeChild(view) {
|
|
100
162
|
if (view && view.dom) {
|
|
@@ -157,14 +219,19 @@ export class TabsView extends QtBaseComponent {
|
|
|
157
219
|
this._tabData = { ...config, data }
|
|
158
220
|
this._tabItems = Array.isArray(data) ? data : []
|
|
159
221
|
this._currentPageIndex = config.defaultIndex ?? 0
|
|
160
|
-
|
|
222
|
+
// 修复:使用正确的属性名 defaultFocusIndex
|
|
223
|
+
this._focusIndex = config.defaultFocusIndex ?? config.focusIndex ?? this._currentPageIndex
|
|
161
224
|
|
|
162
225
|
console.log(
|
|
163
226
|
'[TabsView] setTabsData:',
|
|
164
227
|
config,
|
|
165
228
|
data,
|
|
166
229
|
'tabList:',
|
|
167
|
-
this._childrenByName.get('tabList')
|
|
230
|
+
this._childrenByName.get('tabList'),
|
|
231
|
+
'defaultFocusIndex:',
|
|
232
|
+
config.defaultFocusIndex,
|
|
233
|
+
'_focusIndex:',
|
|
234
|
+
this._focusIndex
|
|
168
235
|
)
|
|
169
236
|
|
|
170
237
|
// 通过 name 获取 tabList (qt-nav-bar) 并设置数据
|
|
@@ -187,10 +254,103 @@ export class TabsView extends QtBaseComponent {
|
|
|
187
254
|
}
|
|
188
255
|
// Dispatch page changed event
|
|
189
256
|
this._dispatchPageChanged(this._currentPageIndex)
|
|
257
|
+
// 分发加载数据事件
|
|
190
258
|
this._dispatchLoadPageData(this._currentPageIndex, false)
|
|
259
|
+
|
|
260
|
+
// 延迟设置焦点,等待 QtFlexView 渲染完成
|
|
261
|
+
setTimeout(() => {
|
|
262
|
+
this._setInitialTabFocus()
|
|
263
|
+
}, 100)
|
|
191
264
|
})
|
|
192
265
|
}
|
|
193
266
|
|
|
267
|
+
// 设置 tab 导航栏的初始焦点
|
|
268
|
+
_setInitialTabFocus() {
|
|
269
|
+
const tabList = this.getChildByName('tabList')
|
|
270
|
+
if (!tabList || !tabList.dom) {
|
|
271
|
+
console.log('[TabsView] _setInitialTabFocus: tabList not found')
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const focusIndex = this._focusIndex
|
|
276
|
+
|
|
277
|
+
// 查找 tabList 中的 itemContainer(实际渲染的项目容器)
|
|
278
|
+
// QtFlexView 使用 _itemContainer 存储渲染的项目
|
|
279
|
+
let itemContainer = tabList._itemContainer
|
|
280
|
+
if (!itemContainer) {
|
|
281
|
+
// 尝试通过 DOM 查找
|
|
282
|
+
itemContainer = tabList.dom.querySelector('.flex-view-item-container')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (itemContainer) {
|
|
286
|
+
// 从 itemContainer 中查找实际渲染的项目
|
|
287
|
+
const renderedItems = itemContainer.children
|
|
288
|
+
console.log(
|
|
289
|
+
'[TabsView] _setInitialTabFocus: itemContainer found, children:',
|
|
290
|
+
renderedItems.length
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if (renderedItems.length > 0 && focusIndex >= 0 && focusIndex < renderedItems.length) {
|
|
294
|
+
const targetWrapper = renderedItems[focusIndex]
|
|
295
|
+
// 在 wrapper 内部查找可聚焦元素
|
|
296
|
+
const focusableItem =
|
|
297
|
+
targetWrapper.querySelector('[focusable="true"], [focusable="1"], [focusable]') ||
|
|
298
|
+
targetWrapper
|
|
299
|
+
|
|
300
|
+
console.log(
|
|
301
|
+
'[TabsView] _setInitialTabFocus: setting focus to rendered item index',
|
|
302
|
+
focusIndex,
|
|
303
|
+
focusableItem
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
// 设置 autofocus 属性
|
|
307
|
+
focusableItem.setAttribute('autofocus', 'true')
|
|
308
|
+
|
|
309
|
+
// 直接触发焦点设置
|
|
310
|
+
if (global.__TV_FOCUS_MANAGER__) {
|
|
311
|
+
// 先标记该页已加载,防止焦点事件触发重复加载
|
|
312
|
+
// 注意:这里不需要,因为焦点设置的是同一个页面(focusIndex === _currentPageIndex)
|
|
313
|
+
global.__TV_FOCUS_MANAGER__.updateFocusableElements()
|
|
314
|
+
global.__TV_FOCUS_MANAGER__._doFocusElement(focusableItem)
|
|
315
|
+
}
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 回退:查找非模板的可聚焦元素
|
|
321
|
+
const allFocusable = tabList.dom.querySelectorAll(
|
|
322
|
+
'[focusable="true"], [focusable="1"], [focusable]'
|
|
323
|
+
)
|
|
324
|
+
const focusableItems = Array.from(allFocusable).filter((el) => {
|
|
325
|
+
// 排除模板元素
|
|
326
|
+
return (
|
|
327
|
+
el.getAttribute('data-template') !== 'true' &&
|
|
328
|
+
el.style.display !== 'none' &&
|
|
329
|
+
!el.closest('[data-template="true"]')
|
|
330
|
+
)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
console.log('[TabsView] _setInitialTabFocus: fallback, focusableItems:', focusableItems.length)
|
|
334
|
+
|
|
335
|
+
if (focusableItems.length > 0 && focusIndex >= 0 && focusIndex < focusableItems.length) {
|
|
336
|
+
const targetItem = focusableItems[focusIndex]
|
|
337
|
+
console.log('[TabsView] _setInitialTabFocus: setting focus to index', focusIndex, targetItem)
|
|
338
|
+
|
|
339
|
+
targetItem.setAttribute('autofocus', 'true')
|
|
340
|
+
|
|
341
|
+
if (global.__TV_FOCUS_MANAGER__) {
|
|
342
|
+
global.__TV_FOCUS_MANAGER__.updateFocusableElements()
|
|
343
|
+
global.__TV_FOCUS_MANAGER__._doFocusElement(targetItem)
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
console.log(
|
|
347
|
+
'[TabsView] _setInitialTabFocus: no focusable items found or invalid index',
|
|
348
|
+
focusIndex,
|
|
349
|
+
focusableItems.length
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
194
354
|
// Set page data - 设置页面内容数据
|
|
195
355
|
setPageData(...args) {
|
|
196
356
|
args = Array.isArray(args[0]) && args.length === 1 ? args[0] : args
|
|
@@ -301,12 +461,12 @@ export class TabsView extends QtBaseComponent {
|
|
|
301
461
|
|
|
302
462
|
// Reload all pages
|
|
303
463
|
reloadAll(updateCurrent = true) {
|
|
304
|
-
this._dispatchLoadPageData(this._currentPageIndex, false)
|
|
464
|
+
this._dispatchLoadPageData(this._currentPageIndex, false, true)
|
|
305
465
|
}
|
|
306
466
|
|
|
307
467
|
// Reload specific page
|
|
308
468
|
reloadPage(pageIndex) {
|
|
309
|
-
this._dispatchLoadPageData(pageIndex, false)
|
|
469
|
+
this._dispatchLoadPageData(pageIndex, false, true)
|
|
310
470
|
}
|
|
311
471
|
|
|
312
472
|
// Invoke content function
|
|
@@ -396,24 +556,67 @@ export class TabsView extends QtBaseComponent {
|
|
|
396
556
|
|
|
397
557
|
// Dispatch page changed event
|
|
398
558
|
_dispatchPageChanged(pageIndex) {
|
|
559
|
+
// 防止重复触发同一页面的 pageChanged 事件
|
|
560
|
+
if (this._lastPageChangedIndex === pageIndex) {
|
|
561
|
+
console.log(
|
|
562
|
+
'[TabsView] _dispatchPageChanged: skipping duplicate page changed for index',
|
|
563
|
+
pageIndex,
|
|
564
|
+
'instanceId:',
|
|
565
|
+
this._instanceId
|
|
566
|
+
)
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
this._lastPageChangedIndex = pageIndex
|
|
570
|
+
|
|
399
571
|
const params = {
|
|
400
572
|
pageIndex,
|
|
401
573
|
data: this._tabItems[pageIndex] || null,
|
|
402
574
|
}
|
|
403
|
-
console.log(
|
|
575
|
+
console.log(
|
|
576
|
+
'[TabsView] _dispatchPageChanged, pageIndex:',
|
|
577
|
+
pageIndex,
|
|
578
|
+
'instanceId:',
|
|
579
|
+
this._instanceId,
|
|
580
|
+
'events:',
|
|
581
|
+
this.events
|
|
582
|
+
)
|
|
404
583
|
// es3-vue 将 @page-changed 转换为 pagechanged 存储在 events 中
|
|
405
584
|
this.dispatchEvent('onPagechanged', params)
|
|
406
585
|
}
|
|
407
586
|
|
|
408
587
|
// Dispatch load page event
|
|
409
|
-
_dispatchLoadPageData(pageIndex, useDiff) {
|
|
588
|
+
_dispatchLoadPageData(pageIndex, useDiff, force = false) {
|
|
589
|
+
// 防止重复加载同一页数据(除非强制刷新)
|
|
590
|
+
if (!force && this._loadedPageIndices.has(pageIndex)) {
|
|
591
|
+
console.log(
|
|
592
|
+
'[TabsView] _dispatchLoadPageData: skipping already loaded page',
|
|
593
|
+
pageIndex,
|
|
594
|
+
'instanceId:',
|
|
595
|
+
this._instanceId
|
|
596
|
+
)
|
|
597
|
+
return
|
|
598
|
+
}
|
|
599
|
+
|
|
410
600
|
const params = {
|
|
411
601
|
pageIndex,
|
|
412
602
|
itemCount: 0,
|
|
413
603
|
data: this._tabItems[pageIndex] || null,
|
|
414
604
|
useDiff: !!useDiff,
|
|
415
605
|
}
|
|
416
|
-
console.log(
|
|
606
|
+
console.log(
|
|
607
|
+
'[TabsView] _dispatchLoadPageData, instanceId:',
|
|
608
|
+
this._instanceId,
|
|
609
|
+
'pageIndex:',
|
|
610
|
+
pageIndex,
|
|
611
|
+
'force:',
|
|
612
|
+
force,
|
|
613
|
+
'alreadyLoaded:',
|
|
614
|
+
this._loadedPageIndices.has(pageIndex),
|
|
615
|
+
'loadedIndices:',
|
|
616
|
+
Array.from(this._loadedPageIndices)
|
|
617
|
+
)
|
|
618
|
+
// 标记该页已加载
|
|
619
|
+
this._loadedPageIndices.add(pageIndex)
|
|
417
620
|
// es3-vue 将 @load-page 转换为 loadpagedata 存储在 events 中
|
|
418
621
|
this.dispatchEvent('onLoadpagedata', params)
|
|
419
622
|
}
|
package/src/index.js
CHANGED
|
@@ -243,4 +243,55 @@ export function startWebEngine(engine) {
|
|
|
243
243
|
console.log('[Web Renderer] Engine started')
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
// Global engine instance for convenience functions
|
|
247
|
+
let _webEngine = null
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Initialize the web renderer (convenience function)
|
|
251
|
+
* Creates and prepares the engine, applies patches
|
|
252
|
+
*/
|
|
253
|
+
export function initWebRenderer() {
|
|
254
|
+
if (_webEngine) {
|
|
255
|
+
console.log('[Web Renderer] Engine already initialized')
|
|
256
|
+
return _webEngine
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log('[Web Renderer] Initializing...')
|
|
260
|
+
_webEngine = createWebEngine()
|
|
261
|
+
|
|
262
|
+
// Apply all patches (from core exports)
|
|
263
|
+
// These patches are auto-applied by core/index.js
|
|
264
|
+
|
|
265
|
+
console.log('[Web Renderer] Initialization complete')
|
|
266
|
+
return _webEngine
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Start the web renderer (convenience function)
|
|
271
|
+
* Starts the previously initialized engine
|
|
272
|
+
*/
|
|
273
|
+
export function startWebRenderer() {
|
|
274
|
+
if (!_webEngine) {
|
|
275
|
+
console.log('[Web Renderer] No engine found, creating one...')
|
|
276
|
+
_webEngine = createWebEngine()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
startWebEngine(_webEngine)
|
|
280
|
+
return _webEngine
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get the current web engine instance
|
|
285
|
+
*/
|
|
286
|
+
export function getWebEngine() {
|
|
287
|
+
return _webEngine
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Set the web engine instance
|
|
292
|
+
*/
|
|
293
|
+
export function setWebEngine(engine) {
|
|
294
|
+
_webEngine = engine
|
|
295
|
+
}
|
|
296
|
+
|
|
246
297
|
export { APP_NAME }
|