@nativescript/core 9.0.0-alpha.23 → 9.0.0-alpha.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/application/application.android.d.ts +9 -2
  2. package/application/application.android.js +83 -6
  3. package/application/application.android.js.map +1 -1
  4. package/application/application.d.ts +8 -1
  5. package/application/application.ios.d.ts +13 -1
  6. package/application/application.ios.js +30 -3
  7. package/application/application.ios.js.map +1 -1
  8. package/application/helpers.android.d.ts +0 -9
  9. package/application/helpers.android.js +0 -54
  10. package/application/helpers.android.js.map +1 -1
  11. package/application/helpers.d.ts +0 -10
  12. package/application/helpers.ios.d.ts +0 -19
  13. package/application/helpers.ios.js +0 -38
  14. package/application/helpers.ios.js.map +1 -1
  15. package/connectivity/index.android.js +3 -3
  16. package/connectivity/index.android.js.map +1 -1
  17. package/core-types/index.d.ts +68 -63
  18. package/core-types/index.js.map +1 -1
  19. package/image-source/index.d.ts +2 -2
  20. package/index.js +0 -1
  21. package/index.js.map +1 -1
  22. package/inspector_modules.js +72 -5
  23. package/inspector_modules.js.map +1 -1
  24. package/package.json +1 -1
  25. package/platforms/android/widgets-release.aar +0 -0
  26. package/references.d.ts +1 -1
  27. package/ui/button/index.ios.js.map +1 -1
  28. package/ui/core/view/index.android.d.ts +4 -6
  29. package/ui/core/view/index.android.js +126 -271
  30. package/ui/core/view/index.android.js.map +1 -1
  31. package/ui/core/view/index.d.ts +1 -1
  32. package/ui/core/view/index.ios.d.ts +7 -0
  33. package/ui/core/view/index.ios.js +74 -35
  34. package/ui/core/view/index.ios.js.map +1 -1
  35. package/ui/core/view/view-common.d.ts +37 -5
  36. package/ui/core/view/view-common.js +21 -0
  37. package/ui/core/view/view-common.js.map +1 -1
  38. package/ui/core/view/view-interfaces.d.ts +3 -0
  39. package/ui/frame/index.android.js +55 -2
  40. package/ui/frame/index.android.js.map +1 -1
  41. package/ui/index.d.ts +1 -1
  42. package/ui/layouts/index.d.ts +2 -0
  43. package/ui/layouts/index.js +2 -0
  44. package/ui/layouts/index.js.map +1 -1
  45. package/ui/layouts/liquid-glass/index.android.d.ts +5 -0
  46. package/ui/layouts/liquid-glass/index.android.js +8 -0
  47. package/ui/layouts/liquid-glass/index.android.js.map +1 -0
  48. package/ui/layouts/liquid-glass/index.d.ts +10 -0
  49. package/ui/layouts/liquid-glass/index.ios.d.ts +10 -0
  50. package/ui/layouts/liquid-glass/index.ios.js +59 -0
  51. package/ui/layouts/liquid-glass/index.ios.js.map +1 -0
  52. package/ui/layouts/liquid-glass/liquid-glass-common.d.ts +3 -0
  53. package/ui/layouts/liquid-glass/liquid-glass-common.js +4 -0
  54. package/ui/layouts/liquid-glass/liquid-glass-common.js.map +1 -0
  55. package/ui/layouts/liquid-glass-container/index.android.d.ts +3 -0
  56. package/ui/layouts/liquid-glass-container/index.android.js +4 -0
  57. package/ui/layouts/liquid-glass-container/index.android.js.map +1 -0
  58. package/ui/layouts/liquid-glass-container/index.d.ts +3 -0
  59. package/ui/layouts/liquid-glass-container/index.ios.d.ts +14 -0
  60. package/ui/layouts/liquid-glass-container/index.ios.js +121 -0
  61. package/ui/layouts/liquid-glass-container/index.ios.js.map +1 -0
  62. package/ui/layouts/liquid-glass-container/liquid-glass-container-common.d.ts +4 -0
  63. package/ui/layouts/liquid-glass-container/liquid-glass-container-common.js +5 -0
  64. package/ui/layouts/liquid-glass-container/liquid-glass-container-common.js.map +1 -0
  65. package/ui/list-view/index.android.d.ts +26 -1
  66. package/ui/list-view/index.android.js +701 -23
  67. package/ui/list-view/index.android.js.map +1 -1
  68. package/ui/list-view/index.d.ts +122 -0
  69. package/ui/list-view/index.ios.d.ts +34 -2
  70. package/ui/list-view/index.ios.js +560 -9
  71. package/ui/list-view/index.ios.js.map +1 -1
  72. package/ui/list-view/list-view-common.d.ts +22 -1
  73. package/ui/list-view/list-view-common.js +85 -0
  74. package/ui/list-view/list-view-common.js.map +1 -1
  75. package/ui/styling/background-common.d.ts +4 -4
  76. package/ui/styling/background-common.js +8 -8
  77. package/ui/styling/background-common.js.map +1 -1
  78. package/ui/styling/background.d.ts +0 -3
  79. package/ui/styling/background.ios.d.ts +2 -1
  80. package/ui/styling/background.ios.js +47 -38
  81. package/ui/styling/background.ios.js.map +1 -1
  82. package/ui/styling/css-utils.d.ts +1 -0
  83. package/ui/styling/css-utils.js +15 -4
  84. package/ui/styling/css-utils.js.map +1 -1
  85. package/ui/styling/style/index.d.ts +2 -1
  86. package/ui/styling/style/index.js.map +1 -1
  87. package/ui/styling/style-properties.js +23 -11
  88. package/ui/styling/style-properties.js.map +1 -1
  89. package/ui/tab-view/index.android.js +22 -8
  90. package/ui/tab-view/index.android.js.map +1 -1
  91. package/ui/tab-view/index.ios.d.ts +1 -1
  92. package/ui/tab-view/index.ios.js +28 -9
  93. package/ui/tab-view/index.ios.js.map +1 -1
  94. package/ui/tab-view/tab-view-common.d.ts +2 -0
  95. package/ui/tab-view/tab-view-common.js +5 -0
  96. package/ui/tab-view/tab-view-common.js.map +1 -1
  97. package/utils/common.d.ts +3 -1
  98. package/utils/common.js +9 -3
  99. package/utils/common.js.map +1 -1
  100. package/utils/index.d.ts +6 -0
  101. package/utils/native-helper-for-android.d.ts +14 -3
  102. package/utils/native-helper-for-android.js +57 -54
  103. package/utils/native-helper-for-android.js.map +1 -1
  104. package/utils/native-helper.android.d.ts +3 -3
  105. package/utils/native-helper.android.js +2 -2
  106. package/utils/native-helper.android.js.map +1 -1
  107. package/utils/native-helper.d.ts +22 -5
@@ -1,4 +1,6 @@
1
- import { ListViewBase, separatorColorProperty, itemTemplatesProperty } from './list-view-common';
1
+ import { ListViewBase, separatorColorProperty, itemTemplatesProperty, stickyHeaderProperty, stickyHeaderTemplateProperty, sectionedProperty, showSearchProperty } from './list-view-common';
2
+ import { View } from '../core/view';
3
+ import { PercentLength } from '../styling/length-shared';
2
4
  import { unsetValue } from '../core/properties/property-shared';
3
5
  import { Color } from '../../color';
4
6
  import { Observable } from '../../data/observable';
@@ -6,10 +8,18 @@ import { StackLayout } from '../layouts/stack-layout';
6
8
  import { ProxyViewContainer } from '../proxy-view-container';
7
9
  import { LayoutBase } from '../layouts/layout-base';
8
10
  import { profile } from '../../profiling';
11
+ import { Trace } from '../../trace';
12
+ import { Builder } from '../builder';
13
+ import { Label } from '../label';
9
14
  export * from './list-view-common';
10
15
  const ITEMLOADING = ListViewBase.itemLoadingEvent;
11
16
  const LOADMOREITEMS = ListViewBase.loadMoreItemsEvent;
12
17
  const ITEMTAP = ListViewBase.itemTapEvent;
18
+ const SEARCHCHANGE = ListViewBase.searchChangeEvent;
19
+ const STICKY_HEADER_Z_INDEX = 1000;
20
+ const SEARCH_VIEW_Z_INDEX = 2000;
21
+ // View type constants for sectioned lists
22
+ const ITEM_VIEW_TYPE = 0;
13
23
  let ItemClickListener;
14
24
  function initializeItemClickListener() {
15
25
  if (ItemClickListener) {
@@ -48,6 +58,11 @@ export class ListView extends ListViewBase {
48
58
  this._realizedItems = new Map();
49
59
  this._availableViews = new Map();
50
60
  this._realizedTemplates = new Map();
61
+ this._stickyHeaderHeight = 0;
62
+ this._hiddenHeaderPositions = new Set(); // Track which headers to hide
63
+ }
64
+ get hasSearchView() {
65
+ return !!this._searchView;
51
66
  }
52
67
  _ensureAvailableViews(templateKey) {
53
68
  if (!this._availableViews.has(templateKey)) {
@@ -122,6 +137,7 @@ export class ListView extends ListViewBase {
122
137
  this._androidViewId = android.view.View.generateViewId();
123
138
  }
124
139
  nativeView.setId(this._androidViewId);
140
+ // Don't setup search here - wait for onLoaded when context is properly available
125
141
  }
126
142
  disposeNativeView() {
127
143
  const nativeView = this.nativeViewProtected;
@@ -132,13 +148,40 @@ export class ListView extends ListViewBase {
132
148
  if (nativeView.adapter) {
133
149
  nativeView.adapter.owner = null;
134
150
  }
151
+ // Cleanup search
152
+ this._cleanupSearchView();
153
+ // Cleanup sticky header
154
+ this._cleanupStickyHeader();
135
155
  this.clearRealizedCells();
136
156
  super.disposeNativeView();
137
157
  }
158
+ _cleanupStickyHeader() {
159
+ // Remove scroll listener
160
+ if (this._scrollListener) {
161
+ this.nativeViewProtected.setOnScrollListener(null);
162
+ this._scrollListener = null;
163
+ }
164
+ // Remove sticky header from parent
165
+ if (this._stickyHeaderView && this._stickyHeaderView.parent) {
166
+ this._stickyHeaderView.parent._removeView(this._stickyHeaderView);
167
+ }
168
+ this._stickyHeaderView = null;
169
+ this._stickyHeaderHeight = 0;
170
+ // Clear hidden headers
171
+ this._hiddenHeaderPositions.clear();
172
+ }
138
173
  onLoaded() {
139
174
  super.onLoaded();
140
175
  // Without this call itemClick won't be fired... :(
141
176
  this.requestLayout();
177
+ // Setup sticky header if enabled
178
+ if (this.stickyHeader && this.sectioned && this.stickyHeaderTemplate) {
179
+ this._setupStickyHeader();
180
+ }
181
+ // Setup search if enabled and not already set up
182
+ if (this.showSearch && !this._searchView && this.nativeViewProtected && this.nativeViewProtected.getAdapter()) {
183
+ this._setupSearchView();
184
+ }
142
185
  }
143
186
  refresh() {
144
187
  const nativeView = this.nativeViewProtected;
@@ -151,7 +194,19 @@ export class ListView extends ListViewBase {
151
194
  view.bindingContext = null;
152
195
  }
153
196
  });
154
- nativeView.getAdapter().notifyDataSetChanged();
197
+ // Safely refresh the adapter - no HeaderViewListAdapter issues since we don't use headers
198
+ const adapter = nativeView.getAdapter();
199
+ if (adapter instanceof android.widget.BaseAdapter) {
200
+ try {
201
+ adapter.notifyDataSetChanged();
202
+ }
203
+ catch (error) {
204
+ if (Trace.isEnabled()) {
205
+ Trace.error('Error refreshing adapter, recreating: ' + error);
206
+ }
207
+ nativeView.setAdapter(new ListViewAdapterClass(this));
208
+ }
209
+ }
155
210
  }
156
211
  scrollToIndex(index) {
157
212
  const nativeView = this.nativeViewProtected;
@@ -212,6 +267,217 @@ export class ListView extends ListViewBase {
212
267
  const end = nativeView.getLastVisiblePosition();
213
268
  return index >= start && index <= end;
214
269
  }
270
+ // Sticky header methods
271
+ _setupStickyHeader() {
272
+ if (!this.stickyHeader || !this.sectioned || !this.stickyHeaderTemplate) {
273
+ return;
274
+ }
275
+ // Create the sticky header view
276
+ this._createStickyHeaderView();
277
+ // Add it as an overlay to the parent
278
+ this._addStickyHeaderToParent();
279
+ // Add padding to ListView so content doesn't hide behind sticky header
280
+ this._addListViewPadding();
281
+ // Setup scroll listener to update header content
282
+ this._setupScrollListener();
283
+ }
284
+ _createStickyHeaderView() {
285
+ if (this._stickyHeaderView) {
286
+ return; // Already created
287
+ }
288
+ // Create header view using the same template as section headers
289
+ if (typeof this.stickyHeaderTemplate === 'string') {
290
+ try {
291
+ this._stickyHeaderView = Builder.parse(this.stickyHeaderTemplate, this);
292
+ }
293
+ catch (error) {
294
+ // Fallback to simple label
295
+ this._stickyHeaderView = new Label();
296
+ this._stickyHeaderView.text = 'Header Error';
297
+ }
298
+ }
299
+ if (!this._stickyHeaderView) {
300
+ // Default header
301
+ this._stickyHeaderView = new Label();
302
+ this._stickyHeaderView.text = 'Section 0';
303
+ }
304
+ // Set initial binding context (section 0)
305
+ this._updateStickyHeader(0);
306
+ }
307
+ _addStickyHeaderToParent() {
308
+ if (!this._stickyHeaderView || !this.parent) {
309
+ return;
310
+ }
311
+ // Remove from current parent if it has one (likely the ListView from Builder.parse)
312
+ if (this._stickyHeaderView.parent) {
313
+ this._stickyHeaderView.parent._removeView(this._stickyHeaderView);
314
+ }
315
+ // Set proper sizing - don't stretch to fill parent
316
+ this._stickyHeaderView.width = { unit: '%', value: 100 };
317
+ this._stickyHeaderView.height = 'auto'; // Let it size to content
318
+ this._stickyHeaderView.verticalAlignment = 'top';
319
+ this._stickyHeaderView.horizontalAlignment = 'stretch';
320
+ // Add sticky header to the parent layout
321
+ // If search view exists, position sticky header after it (index 1), otherwise at top (index 0)
322
+ const parentLayout = this.parent;
323
+ const hasSearchView = this.showSearch && this._searchView && this._searchView._wrapper;
324
+ if (parentLayout instanceof StackLayout) {
325
+ const insertIndex = hasSearchView ? 1 : 0;
326
+ parentLayout.insertChild(this._stickyHeaderView, insertIndex);
327
+ }
328
+ else {
329
+ parentLayout._addView(this._stickyHeaderView);
330
+ }
331
+ // When search is enabled, position sticky header below search view with proper top margin
332
+ if (this.showSearch && this._searchView) {
333
+ // Add top margin to push sticky header below search view
334
+ this._stickyHeaderView.marginTop = 0; // Reset any previous margin
335
+ // Position sticky header with proper offset using native positioning
336
+ if (this._stickyHeaderView.nativeViewProtected) {
337
+ this._stickyHeaderView.nativeViewProtected.setZ(STICKY_HEADER_Z_INDEX);
338
+ // Use a timeout to ensure search view is measured first
339
+ setTimeout(() => {
340
+ if (this._searchView && this._searchView._wrapper) {
341
+ const searchWrapper = this._searchView._wrapper;
342
+ if (searchWrapper.nativeViewProtected) {
343
+ const searchHeight = searchWrapper.nativeViewProtected.getMeasuredHeight() || 50;
344
+ // Position sticky header below search view using translation
345
+ if (this._stickyHeaderView.nativeViewProtected) {
346
+ this._stickyHeaderView.nativeViewProtected.setTranslationY(searchHeight);
347
+ }
348
+ }
349
+ }
350
+ }, 100);
351
+ }
352
+ }
353
+ else {
354
+ // No search view - position at top
355
+ if (this._stickyHeaderView.nativeViewProtected) {
356
+ this._stickyHeaderView.nativeViewProtected.setZ(STICKY_HEADER_Z_INDEX);
357
+ this._stickyHeaderView.nativeViewProtected.setTranslationY(0);
358
+ }
359
+ }
360
+ }
361
+ _addListViewPadding() {
362
+ if (!this._stickyHeaderView) {
363
+ return;
364
+ }
365
+ // Calculate total top padding: search view height + sticky header height
366
+ let searchViewHeight = 0;
367
+ if (this.showSearch && this._searchView && this._searchView._wrapper) {
368
+ const searchWrapper = this._searchView._wrapper;
369
+ if (searchWrapper.nativeViewProtected && searchWrapper.nativeViewProtected.getMeasuredHeight() > 0) {
370
+ searchViewHeight = searchWrapper.nativeViewProtected.getMeasuredHeight();
371
+ }
372
+ else {
373
+ searchViewHeight = 50; // Default search view height
374
+ }
375
+ }
376
+ // Apply immediate padding with defaults to prevent content hiding
377
+ const defaultHeaderHeight = 50; // Reasonable default height in dp
378
+ const totalPadding = searchViewHeight + defaultHeaderHeight;
379
+ this.nativeViewProtected.setPadding(0, totalPadding, 0, 0);
380
+ this._stickyHeaderHeight = defaultHeaderHeight;
381
+ // Request layout to ensure proper measurement
382
+ this._stickyHeaderView.requestLayout();
383
+ // Then measure and adjust padding if needed using a layout listener for determinism
384
+ const stickyHeaderNativeView = this._stickyHeaderView && this._stickyHeaderView.nativeViewProtected;
385
+ if (stickyHeaderNativeView) {
386
+ const layoutListener = new android.view.View.OnLayoutChangeListener({
387
+ onLayoutChange: (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) => {
388
+ if (v.getMeasuredHeight() > 0) {
389
+ const measuredHeaderHeight = v.getMeasuredHeight();
390
+ let finalSearchHeight = searchViewHeight;
391
+ // Re-measure search view if needed
392
+ if (this.showSearch && this._searchView && this._searchView._wrapper) {
393
+ const searchWrapper = this._searchView._wrapper;
394
+ if (searchWrapper.nativeViewProtected && searchWrapper.nativeViewProtected.getMeasuredHeight() > 0) {
395
+ finalSearchHeight = searchWrapper.nativeViewProtected.getMeasuredHeight();
396
+ }
397
+ }
398
+ // Calculate final padding: search height + sticky header height + small buffer
399
+ const totalPaddingHeight = finalSearchHeight + measuredHeaderHeight + 4;
400
+ this._stickyHeaderHeight = measuredHeaderHeight;
401
+ this.nativeViewProtected.setPadding(0, totalPaddingHeight, 0, 0);
402
+ this.scrollToIndex(0);
403
+ // Remove the listener after first valid layout
404
+ v.removeOnLayoutChangeListener(layoutListener);
405
+ }
406
+ },
407
+ });
408
+ stickyHeaderNativeView.addOnLayoutChangeListener(layoutListener);
409
+ }
410
+ }
411
+ _setupScrollListener() {
412
+ if (this._scrollListener) {
413
+ return; // Already setup
414
+ }
415
+ const owner = this;
416
+ this._scrollListener = new android.widget.AbsListView.OnScrollListener({
417
+ onScrollStateChanged(view, scrollState) {
418
+ // Not needed for sticky headers
419
+ },
420
+ onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount) {
421
+ if (owner.sectioned && owner._stickyHeaderView) {
422
+ const currentSection = owner._getCurrentSection(firstVisibleItem);
423
+ owner._updateStickyHeader(currentSection);
424
+ // Hide section headers when they would appear right below sticky header
425
+ owner._updateHiddenHeaders(firstVisibleItem);
426
+ }
427
+ },
428
+ });
429
+ this.nativeViewProtected.setOnScrollListener(this._scrollListener);
430
+ }
431
+ _getCurrentSection(firstVisibleItem) {
432
+ if (!this.sectioned) {
433
+ return 0;
434
+ }
435
+ // Convert the first visible list position to section number
436
+ let currentPosition = 0;
437
+ const sectionCount = this._getSectionCount();
438
+ for (let section = 0; section < sectionCount; section++) {
439
+ // Check if firstVisibleItem is in this section (header or items)
440
+ const sectionItems = this._getItemsInSection(section) || [];
441
+ const itemsInSection = sectionItems.length || 0;
442
+ const sectionEndPosition = currentPosition + 1 + itemsInSection; // +1 for header
443
+ if (firstVisibleItem < sectionEndPosition) {
444
+ return section;
445
+ }
446
+ currentPosition = sectionEndPosition;
447
+ }
448
+ return Math.max(0, sectionCount - 1); // Fallback to last section
449
+ }
450
+ _updateStickyHeader(section) {
451
+ if (!this._stickyHeaderView) {
452
+ return;
453
+ }
454
+ // Update binding context to match the current section
455
+ const sectionData = this._getSectionData(section);
456
+ if (sectionData) {
457
+ this._stickyHeaderView.bindingContext = sectionData;
458
+ }
459
+ else {
460
+ this._stickyHeaderView.bindingContext = { title: `Section ${section}`, section: section };
461
+ }
462
+ }
463
+ _updateHiddenHeaders(firstVisibleItem) {
464
+ const previousHiddenHeaders = new Set(this._hiddenHeaderPositions);
465
+ this._hiddenHeaderPositions.clear();
466
+ // If we're at the very top (first item is position 0, which is the first section header),
467
+ // hide that section header to avoid duplication with sticky header
468
+ if (firstVisibleItem === 0) {
469
+ this._hiddenHeaderPositions.add(0); // Hide the first section header position
470
+ }
471
+ // If hidden headers changed, refresh the adapter
472
+ const hiddenHeadersChanged = previousHiddenHeaders.size !== this._hiddenHeaderPositions.size || [...previousHiddenHeaders].some((pos) => !this._hiddenHeaderPositions.has(pos));
473
+ if (hiddenHeadersChanged) {
474
+ // Refresh adapter to update visibility
475
+ const adapter = this.nativeViewProtected.getAdapter();
476
+ if (adapter instanceof android.widget.BaseAdapter) {
477
+ adapter.notifyDataSetChanged();
478
+ }
479
+ }
480
+ }
215
481
  [separatorColorProperty.getDefault]() {
216
482
  const nativeView = this.nativeViewProtected;
217
483
  return {
@@ -238,8 +504,199 @@ export class ListView extends ListViewBase {
238
504
  if (value) {
239
505
  this._itemTemplatesInternal = this._itemTemplatesInternal.concat(value);
240
506
  }
241
- this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
242
- this.refresh();
507
+ if (this.nativeViewProtected) {
508
+ this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
509
+ this.refresh();
510
+ }
511
+ }
512
+ // Sticky header property handlers
513
+ [stickyHeaderProperty.setNative](value) {
514
+ // Refresh adapter to handle sectioned vs non-sectioned display
515
+ if (this.nativeViewProtected && this.nativeViewProtected.getAdapter()) {
516
+ this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
517
+ }
518
+ // Setup or cleanup sticky header
519
+ if (value && this.sectioned && this.stickyHeaderTemplate && this.isLoaded) {
520
+ this._setupStickyHeader();
521
+ }
522
+ else {
523
+ this._cleanupStickyHeader();
524
+ }
525
+ }
526
+ [stickyHeaderTemplateProperty.setNative](value) {
527
+ // Refresh adapter when template changes
528
+ if (this.nativeViewProtected && this.nativeViewProtected.getAdapter()) {
529
+ this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
530
+ }
531
+ // Recreate sticky header with new template
532
+ this._cleanupStickyHeader();
533
+ if (value && this.stickyHeader && this.sectioned && this.isLoaded) {
534
+ this._setupStickyHeader();
535
+ }
536
+ }
537
+ [sectionedProperty.setNative](value) {
538
+ // Refresh adapter to handle sectioned vs non-sectioned data
539
+ if (this.nativeViewProtected && this.nativeViewProtected.getAdapter()) {
540
+ this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
541
+ }
542
+ // Setup or cleanup sticky header based on sectioned state
543
+ if (value && this.stickyHeader && this.stickyHeaderTemplate && this.isLoaded) {
544
+ this._setupStickyHeader();
545
+ }
546
+ else {
547
+ this._cleanupStickyHeader();
548
+ }
549
+ }
550
+ // Search methods
551
+ _setupSearchView() {
552
+ if (this._searchView || !this.showSearch || !this.nativeViewProtected) {
553
+ return;
554
+ }
555
+ // Create SearchView using the ListView's context
556
+ this._searchView = new android.widget.SearchView(this.nativeViewProtected.getContext());
557
+ this._searchView.setQueryHint('Search...');
558
+ this._searchView.setIconifiedByDefault(false);
559
+ this._searchView.setSubmitButtonEnabled(false);
560
+ // Setup search listener
561
+ const owner = this;
562
+ this._searchListener = new android.widget.SearchView.OnQueryTextListener({
563
+ onQueryTextChange(newText) {
564
+ const args = {
565
+ eventName: SEARCHCHANGE,
566
+ object: owner,
567
+ text: newText,
568
+ android: owner._searchView,
569
+ };
570
+ owner.notify(args);
571
+ return true;
572
+ },
573
+ onQueryTextSubmit(query) {
574
+ const args = {
575
+ eventName: SEARCHCHANGE,
576
+ object: owner,
577
+ text: query,
578
+ android: owner._searchView,
579
+ };
580
+ owner.notify(args);
581
+ return true;
582
+ },
583
+ });
584
+ this._searchView.setOnQueryTextListener(this._searchListener);
585
+ // Add search view to the parent container above the ListView
586
+ this._addSearchToParent();
587
+ // Add padding to ListView if no sticky header (otherwise sticky header method handles it)
588
+ if (!this.stickyHeader || !this._stickyHeaderView) {
589
+ this._addSearchPadding();
590
+ }
591
+ }
592
+ _addSearchPadding() {
593
+ if (!this._searchView) {
594
+ return;
595
+ }
596
+ // Add basic padding for search view
597
+ const defaultSearchHeight = 50; // Default search view height
598
+ this.nativeViewProtected.setPadding(0, defaultSearchHeight, 0, 0);
599
+ // Measure and adjust if needed
600
+ setTimeout(() => {
601
+ if (this._searchView && this._searchView._wrapper) {
602
+ const searchWrapper = this._searchView._wrapper;
603
+ if (searchWrapper.nativeViewProtected && searchWrapper.nativeViewProtected.getMeasuredHeight() > 0) {
604
+ const measuredHeight = searchWrapper.nativeViewProtected.getMeasuredHeight();
605
+ this.nativeViewProtected.setPadding(0, measuredHeight + 4, 0, 0);
606
+ }
607
+ }
608
+ }, 100);
609
+ }
610
+ _addSearchToParent() {
611
+ if (!this._searchView || !this.parent) {
612
+ return;
613
+ }
614
+ // Get the parent layout
615
+ const parentLayout = this.parent;
616
+ // Create a simple NativeScript wrapper for the native SearchView
617
+ const searchView = this._searchView;
618
+ const searchViewWrapper = new (class extends View {
619
+ createNativeView() {
620
+ return searchView;
621
+ }
622
+ })();
623
+ // Set layout properties - ensure it's at the top
624
+ searchViewWrapper.height = 'auto';
625
+ searchViewWrapper.width = { unit: '%', value: 100 };
626
+ searchViewWrapper.verticalAlignment = 'top';
627
+ searchViewWrapper.horizontalAlignment = 'stretch';
628
+ // Always insert at position 0 (top) regardless of ListView position
629
+ if (parentLayout instanceof StackLayout) {
630
+ parentLayout.insertChild(searchViewWrapper, 0);
631
+ }
632
+ else {
633
+ // For other layout types, add as first child
634
+ parentLayout._addView(searchViewWrapper);
635
+ }
636
+ // Ensure search view appears above everything else
637
+ if (searchViewWrapper.nativeViewProtected) {
638
+ searchViewWrapper.nativeViewProtected.setZ(SEARCH_VIEW_Z_INDEX);
639
+ }
640
+ // Store reference for cleanup
641
+ this._searchView._wrapper = searchViewWrapper;
642
+ }
643
+ _cleanupSearchView() {
644
+ if (this._searchView) {
645
+ // Remove search view wrapper from parent
646
+ const wrapper = this._searchView._wrapper;
647
+ if (wrapper && wrapper.parent) {
648
+ wrapper.parent._removeView(wrapper);
649
+ }
650
+ // Clear listener
651
+ if (this._searchListener) {
652
+ this._searchView.setOnQueryTextListener(null);
653
+ this._searchListener = null;
654
+ }
655
+ this._searchView = null;
656
+ // Reset ListView padding if no sticky header
657
+ if (!this.stickyHeader || !this._stickyHeaderView) {
658
+ this.nativeViewProtected.setPadding(0, 0, 0, 0);
659
+ }
660
+ }
661
+ }
662
+ [showSearchProperty.setNative](value) {
663
+ if (value) {
664
+ if (this.isLoaded && this.nativeViewProtected && this.nativeViewProtected.getAdapter()) {
665
+ this._setupSearchView();
666
+ // Reposition sticky header if it exists
667
+ if (this._stickyHeaderView) {
668
+ this._repositionStickyHeader();
669
+ }
670
+ }
671
+ }
672
+ else {
673
+ this._cleanupSearchView();
674
+ // Reposition sticky header if it exists
675
+ if (this._stickyHeaderView) {
676
+ this._repositionStickyHeader();
677
+ }
678
+ }
679
+ }
680
+ _repositionStickyHeader() {
681
+ if (!this._stickyHeaderView || !this._stickyHeaderView.nativeViewProtected) {
682
+ return;
683
+ }
684
+ // Reset positioning
685
+ this._stickyHeaderView.nativeViewProtected.setTranslationY(0);
686
+ // If search is enabled, position below search view
687
+ if (this.showSearch && this._searchView && this._searchView._wrapper) {
688
+ setTimeout(() => {
689
+ const searchWrapper = this._searchView._wrapper;
690
+ if (searchWrapper.nativeViewProtected) {
691
+ const searchHeight = searchWrapper.nativeViewProtected.getMeasuredHeight() || 50;
692
+ this._stickyHeaderView.nativeViewProtected.setTranslationY(searchHeight);
693
+ }
694
+ }, 100);
695
+ }
696
+ // Update ListView padding
697
+ if (this.stickyHeader && this._stickyHeaderView) {
698
+ this._addListViewPadding();
699
+ }
243
700
  }
244
701
  }
245
702
  __decorate([
@@ -261,15 +718,89 @@ function ensureListViewAdapterClass() {
261
718
  return global.__native(_this);
262
719
  }
263
720
  ListViewAdapter.prototype.getCount = function () {
264
- return this.owner && this.owner.items && this.owner.items.length ? this.owner.items.length : 0;
721
+ if (!this.owner) {
722
+ return 0;
723
+ }
724
+ // Ensure we always have at least the items array length, even if empty
725
+ var count = 0;
726
+ if (this.owner.sectioned) {
727
+ // If items are not ready, report 0 to avoid early crashes
728
+ var sectionCount = this.owner._getSectionCount();
729
+ if (!this.owner.items || sectionCount <= 0) {
730
+ return 0;
731
+ }
732
+ // Count items + section headers
733
+ for (var i = 0; i < sectionCount; i++) {
734
+ var itemsInSection = this.owner._getItemsInSection(i) || [];
735
+ // Only add header if section has items
736
+ if (itemsInSection.length > 0) {
737
+ count += 1; // Section header
738
+ count += itemsInSection.length; // Items in section
739
+ }
740
+ }
741
+ }
742
+ else {
743
+ var src = this.owner.items;
744
+ count = src && typeof src.length === 'number' ? src.length : 0;
745
+ }
746
+ // Return the count, ensuring it's never negative
747
+ return Math.max(0, count);
265
748
  };
266
749
  ListViewAdapter.prototype.getItem = function (i) {
267
- if (this.owner && this.owner.items && i < this.owner.items.length) {
268
- var getItem = this.owner.items.getItem;
269
- return getItem ? getItem.call(this.owner.items, i) : this.owner.items[i];
750
+ if (!this.owner || !this.owner.items) {
751
+ return null;
752
+ }
753
+ // Safety check for index bounds
754
+ var totalCount = this.getCount();
755
+ if (i < 0 || i >= totalCount) {
756
+ return null;
757
+ }
758
+ if (this.owner.sectioned) {
759
+ var positionInfo = this._getPositionInfo(i);
760
+ if (positionInfo.isHeader) {
761
+ return this.owner._getSectionData(positionInfo.section);
762
+ }
763
+ else {
764
+ return this.owner._getDataItemInSection(positionInfo.section, positionInfo.itemIndex);
765
+ }
766
+ }
767
+ else {
768
+ var src = this.owner.items;
769
+ if (src && typeof src.length === 'number' && i < src.length) {
770
+ var getItem = src.getItem;
771
+ return getItem ? getItem.call(src, i) : src[i];
772
+ }
270
773
  }
271
774
  return null;
272
775
  };
776
+ // Helper method to determine if position is header and get section/item info
777
+ ListViewAdapter.prototype._getPositionInfo = function (position) {
778
+ if (!this.owner.sectioned) {
779
+ return { isHeader: false, section: 0, itemIndex: position };
780
+ }
781
+ var currentPosition = 0;
782
+ var sectionCount = this.owner._getSectionCount();
783
+ for (var section = 0; section < sectionCount; section++) {
784
+ var itemsInSection = this.owner._getItemsInSection(section) || [];
785
+ // Skip sections with no items (they won't have headers in our count)
786
+ if (itemsInSection.length === 0) {
787
+ continue;
788
+ }
789
+ // Check if this position is the section header
790
+ if (currentPosition === position) {
791
+ return { isHeader: true, section: section, itemIndex: -1 };
792
+ }
793
+ currentPosition++; // Move past header
794
+ // Check if position is within this section's items
795
+ if (position < currentPosition + itemsInSection.length) {
796
+ var itemIndex = position - currentPosition;
797
+ return { isHeader: false, section: section, itemIndex: itemIndex };
798
+ }
799
+ currentPosition += itemsInSection.length; // Move past items
800
+ }
801
+ // Fallback - should not reach here with proper bounds checking
802
+ return { isHeader: false, section: 0, itemIndex: 0 };
803
+ };
273
804
  ListViewAdapter.prototype.getItemId = function (i) {
274
805
  var item = this.getItem(i);
275
806
  var id = i;
@@ -281,28 +812,152 @@ function ensureListViewAdapterClass() {
281
812
  ListViewAdapter.prototype.hasStableIds = function () {
282
813
  return true;
283
814
  };
815
+ ListViewAdapter.prototype.isEnabled = function (position) {
816
+ // Safety check to prevent crashes when adapter is empty
817
+ var totalCount = this.getCount();
818
+ if (totalCount === 0 || position < 0 || position >= totalCount) {
819
+ return false;
820
+ }
821
+ // For sectioned lists, check if this is a header position
822
+ if (this.owner.sectioned) {
823
+ var positionInfo = this._getPositionInfo(position);
824
+ // Headers are typically not clickable, items are
825
+ return !positionInfo.isHeader;
826
+ }
827
+ return true;
828
+ };
284
829
  ListViewAdapter.prototype.getViewTypeCount = function () {
285
- return this.owner._itemTemplatesInternal.length;
830
+ var count = this.owner._itemTemplatesInternal.length;
831
+ // Add 1 for header view type when sectioned
832
+ if (this.owner.sectioned && this.owner.stickyHeaderTemplate) {
833
+ count += 1;
834
+ }
835
+ return count;
286
836
  };
287
837
  ListViewAdapter.prototype.getItemViewType = function (index) {
288
- var template = this.owner._getItemTemplate(index);
289
- var itemViewType = this.owner._itemTemplatesInternal.indexOf(template);
290
- return itemViewType;
838
+ if (this.owner.sectioned) {
839
+ var positionInfo = this._getPositionInfo(index);
840
+ if (positionInfo.isHeader) {
841
+ // Header view type is the last index (after all item template types)
842
+ return this.owner._itemTemplatesInternal.length;
843
+ }
844
+ else {
845
+ // Get template for the actual item
846
+ var template = this.owner._getItemTemplate(positionInfo.itemIndex);
847
+ return this.owner._itemTemplatesInternal.indexOf(template);
848
+ }
849
+ }
850
+ else {
851
+ var template = this.owner._getItemTemplate(index);
852
+ return this.owner._itemTemplatesInternal.indexOf(template);
853
+ }
291
854
  };
292
855
  ListViewAdapter.prototype.getView = function (index, convertView, parent) {
293
856
  //this.owner._dumpRealizedTemplates();
294
857
  if (!this.owner) {
295
858
  return null;
296
859
  }
297
- var totalItemCount = this.owner.items ? this.owner.items.length : 0;
298
- if (index === totalItemCount - 1) {
860
+ // Safety check for empty adapter
861
+ var totalCount = this.getCount();
862
+ if (index < 0 || index >= totalCount) {
863
+ // Return a minimal empty view to prevent crashes
864
+ var emptyView = new android.view.View(this.owner._context);
865
+ var layoutParams = new android.view.ViewGroup.LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, 0);
866
+ emptyView.setLayoutParams(layoutParams);
867
+ return emptyView;
868
+ }
869
+ // Trigger loadMoreItems when binding the last visible row (matches prior Android behavior)
870
+ if (index === totalCount - 1) {
299
871
  this.owner.notify({
300
872
  eventName: LOADMOREITEMS,
301
873
  object: this.owner,
302
874
  });
303
875
  }
304
- // Recycle an existing view or create a new one if needed.
305
- var template = this.owner._getItemTemplate(index);
876
+ if (this.owner.sectioned) {
877
+ var positionInfo = this._getPositionInfo(index);
878
+ if (positionInfo.isHeader) {
879
+ // Create section header view (pass index for hiding logic)
880
+ return this._createHeaderView(positionInfo.section, convertView, parent, index);
881
+ }
882
+ else {
883
+ // Create regular item view with adjusted index
884
+ return this._createItemView(positionInfo.section, positionInfo.itemIndex, convertView, parent);
885
+ }
886
+ }
887
+ else {
888
+ // Non-sectioned - use original logic
889
+ return this._createItemView(0, index, convertView, parent);
890
+ }
891
+ };
892
+ ListViewAdapter.prototype._createHeaderView = function (section, convertView, parent, index) {
893
+ // Check if this header should be hidden to avoid duplication with sticky header
894
+ if (this.owner._hiddenHeaderPositions.has(index)) {
895
+ // Return an empty view with zero height
896
+ var emptyView = new android.view.View(this.owner._context);
897
+ var layoutParams = new android.view.ViewGroup.LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, 0);
898
+ emptyView.setLayoutParams(layoutParams);
899
+ return emptyView;
900
+ }
901
+ var headerView = null;
902
+ var headerViewType = this.owner._itemTemplatesInternal.length; // Same as getItemViewType for headers
903
+ // Try to reuse convertView if it's the right type
904
+ if (convertView) {
905
+ var existingData = this.owner._realizedItems.get(convertView);
906
+ if (existingData && existingData.templateKey === "header_".concat(headerViewType)) {
907
+ headerView = existingData.view;
908
+ }
909
+ }
910
+ // Create new header view if we can't reuse
911
+ if (!headerView) {
912
+ if (this.owner.stickyHeaderTemplate) {
913
+ if (typeof this.owner.stickyHeaderTemplate === 'string') {
914
+ try {
915
+ headerView = Builder.parse(this.owner.stickyHeaderTemplate, this.owner);
916
+ }
917
+ catch (error) {
918
+ // Fallback to simple label
919
+ headerView = new Label();
920
+ headerView.text = 'Header Error';
921
+ }
922
+ }
923
+ }
924
+ if (!headerView) {
925
+ // Default header
926
+ headerView = new Label();
927
+ headerView.text = "Section ".concat(section);
928
+ }
929
+ // Add to parent and get native view
930
+ if (!headerView.parent) {
931
+ if (headerView instanceof LayoutBase && !(headerView instanceof ProxyViewContainer)) {
932
+ this.owner._addView(headerView);
933
+ convertView = headerView.nativeViewProtected;
934
+ }
935
+ else {
936
+ var sp = new StackLayout();
937
+ sp.addChild(headerView);
938
+ this.owner._addView(sp);
939
+ convertView = sp.nativeViewProtected;
940
+ }
941
+ }
942
+ // Register the header view for recycling
943
+ this.owner._realizedItems.set(convertView, {
944
+ view: headerView,
945
+ templateKey: "header_".concat(headerViewType),
946
+ });
947
+ }
948
+ // Set binding context to section data (always update, even for recycled views)
949
+ var sectionData = this.owner._getSectionData(section);
950
+ if (sectionData) {
951
+ headerView.bindingContext = sectionData;
952
+ }
953
+ else {
954
+ headerView.bindingContext = { title: "Section ".concat(section), section: section };
955
+ }
956
+ return convertView;
957
+ };
958
+ ListViewAdapter.prototype._createItemView = function (section, itemIndex, convertView, parent) {
959
+ // Use existing item creation logic but with sectioned data
960
+ var template = this.owner._getItemTemplate(itemIndex);
306
961
  var view;
307
962
  // convertView is of the wrong type
308
963
  if (convertView && this.owner._getKeyFromView(convertView) !== template.key) {
@@ -318,14 +973,14 @@ function ensureListViewAdapterClass() {
318
973
  var args = {
319
974
  eventName: ITEMLOADING,
320
975
  object: this.owner,
321
- index: index,
976
+ index: itemIndex,
322
977
  view: view,
323
978
  android: parent,
324
979
  ios: undefined,
325
980
  };
326
981
  this.owner.notify(args);
327
982
  if (!args.view) {
328
- args.view = this.owner._getDefaultItemContent(index);
983
+ args.view = this.owner._getDefaultItemContent(itemIndex);
329
984
  }
330
985
  if (args.view) {
331
986
  if (this.owner._effectiveRowHeight > -1) {
@@ -334,13 +989,36 @@ function ensureListViewAdapterClass() {
334
989
  else {
335
990
  args.view.height = unsetValue;
336
991
  }
337
- this.owner._prepareItem(args.view, index);
992
+ // Use sectioned item preparation
993
+ if (this.owner.sectioned) {
994
+ this.owner._prepareItemInSection(args.view, section, itemIndex);
995
+ }
996
+ else {
997
+ this.owner._prepareItem(args.view, itemIndex);
998
+ }
338
999
  if (!args.view.parent) {
339
- // Proxy containers should not get treated as layouts.
340
- // Wrap them in a real layout as well.
1000
+ // Ensure margins defined on the template root are honored on Android ListView.
1001
+ // ListView's children don't support layout margins, so we insert an outer wrapper
1002
+ // and keep the original view (with its margins) inside. This mirrors iOS spacing.
1003
+ // If the view is already a LayoutBase (typical for templates like GridLayout),
1004
+ // we wrap it so its margins take effect. For non-layout roots (labels, etc.)
1005
+ // we already wrap below with a StackLayout.
341
1006
  if (args.view instanceof LayoutBase && !(args.view instanceof ProxyViewContainer)) {
342
- this.owner._addView(args.view);
343
- convertView = args.view.nativeViewProtected;
1007
+ var mt = PercentLength.toDevicePixels(args.view.marginTop, 0, Number.NaN);
1008
+ var mb = PercentLength.toDevicePixels(args.view.marginBottom, 0, Number.NaN);
1009
+ var ml = PercentLength.toDevicePixels(args.view.marginLeft, 0, Number.NaN);
1010
+ var mr = PercentLength.toDevicePixels(args.view.marginRight, 0, Number.NaN);
1011
+ var hasMargins = mt > 0 || mb > 0 || ml > 0 || mr > 0;
1012
+ if (hasMargins) {
1013
+ var outer = new StackLayout();
1014
+ outer.addChild(args.view);
1015
+ this.owner._addView(outer);
1016
+ convertView = outer.nativeViewProtected;
1017
+ }
1018
+ else {
1019
+ this.owner._addView(args.view);
1020
+ convertView = args.view.nativeViewProtected;
1021
+ }
344
1022
  }
345
1023
  else {
346
1024
  var sp = new StackLayout();