diff --git a/apps/toolbox/src/main.ts b/apps/toolbox/src/main.ts index a4c5c529a..4e535ec57 100644 --- a/apps/toolbox/src/main.ts +++ b/apps/toolbox/src/main.ts @@ -1,3 +1,4 @@ -import { Application } from '@nativescript/core'; - +import { Application, Trace } from '@nativescript/core'; +Trace.enable(); +Trace.addCategories(Trace.categories.Debug); Application.run({ moduleName: 'app-root' }); diff --git a/apps/toolbox/src/pages/list-page-model.ts b/apps/toolbox/src/pages/list-page-model.ts index 564a67d24..aa7d9646b 100644 --- a/apps/toolbox/src/pages/list-page-model.ts +++ b/apps/toolbox/src/pages/list-page-model.ts @@ -1,21 +1,1377 @@ -import { Observable, Dialogs, DialogStrings } from '@nativescript/core'; +import { Observable, Dialogs, DialogStrings, View, EventData } from '@nativescript/core'; export class ListPageModel extends Observable { components: Array = [ - { name: 'Button', iconText: 'p' }, - { name: 'Image', iconText: 'q' }, - { name: 'Label', iconText: 't' }, - { name: 'Switch', iconText: 'z' }, - { name: 'Slider', iconText: 'v' }, - { name: 'TextField', iconText: 'x' }, - { name: 'TextView', iconText: 'w' }, - { name: 'DatePicker', iconText: 'A' }, - { name: 'Chart', iconText: 'B' }, - { name: 'ListView', iconText: 'u' }, - { name: 'Accelerometer', iconText: 'E' }, - { name: 'Location', iconText: 'D' }, - { name: 'Camera', iconText: String.fromCharCode(parseInt('e034', 16)) }, - { name: 'ImagePicker', iconText: 'q' }, + { + title: 'A', + items: [ + { + name: 'Afghanistan', + code: '(AF)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ซ', + }, + { + name: 'Albania', + code: '(AL)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ฑ', + }, + { + name: 'Algeria', + code: '(DZ)', + flag: '๐Ÿ‡ฉ๐Ÿ‡ฟ', + }, + { + name: 'American Samoa', + code: '(AS)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ธ', + }, + { + name: 'Andorra', + code: '(AD)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ฉ', + }, + { + name: 'Angola', + code: '(AO)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ด', + }, + { + name: 'Anguilla', + code: '(AI)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ฎ', + }, + { + name: 'Antarctica', + code: '(AQ)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ถ', + }, + { + name: 'Antigua and Barbuda', + code: '(AG)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ฌ', + }, + { + name: 'Argentina', + code: '(AR)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ท', + }, + { + name: 'Armenia', + code: '(AM)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ฒ', + }, + { + name: 'Aruba', + code: '(AW)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ผ', + }, + { + name: 'Australia', + code: '(AU)', + flag: '๐Ÿ‡ฆ๐Ÿ‡บ', + }, + { + name: 'Austria', + code: '(AT)', + flag: '๐Ÿ‡ฆ๐Ÿ‡น', + }, + { + name: 'Azerbaijan', + code: '(AZ)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ฟ', + }, + ], + }, + { + title: 'B', + items: [ + { + name: 'Bahamas', + code: '(BS)', + flag: '๐Ÿ‡ง๐Ÿ‡ธ', + }, + { + name: 'Bahrain', + code: '(BH)', + flag: '๐Ÿ‡ง๐Ÿ‡ญ', + }, + { + name: 'Bangladesh', + code: '(BD)', + flag: '๐Ÿ‡ง๐Ÿ‡ฉ', + }, + { + name: 'Barbados', + code: '(BB)', + flag: '๐Ÿ‡ง๐Ÿ‡ง', + }, + { + name: 'Belarus', + code: '(BY)', + flag: '๐Ÿ‡ง๐Ÿ‡พ', + }, + { + name: 'Belgium', + code: '(BE)', + flag: '๐Ÿ‡ง๐Ÿ‡ช', + }, + { + name: 'Belize', + code: '(BZ)', + flag: '๐Ÿ‡ง๐Ÿ‡ฟ', + }, + { + name: 'Benin', + code: '(BJ)', + flag: '๐Ÿ‡ง๐Ÿ‡ฏ', + }, + { + name: 'Bermuda', + code: '(BM)', + flag: '๐Ÿ‡ง๐Ÿ‡ฒ', + }, + { + name: 'Bhutan', + code: '(BT)', + flag: '๐Ÿ‡ง๐Ÿ‡น', + }, + { + name: 'Bolivia', + code: '(BO)', + flag: '๐Ÿ‡ง๐Ÿ‡ด', + }, + { + name: 'Bonaire, Sint Eustatius and Saba', + code: '(BQ)', + flag: '๐Ÿ‡ง๐Ÿ‡ถ', + }, + { + name: 'Bosnia and Herzegovina', + code: '(BA)', + flag: '๐Ÿ‡ง๐Ÿ‡ฆ', + }, + { + name: 'Botswana', + code: '(BW)', + flag: '๐Ÿ‡ง๐Ÿ‡ผ', + }, + { + name: 'Bouvet Island', + code: '(BV)', + flag: '๐Ÿ‡ง๐Ÿ‡ป', + }, + { + name: 'Brazil', + code: '(BR)', + flag: '๐Ÿ‡ง๐Ÿ‡ท', + }, + { + name: 'British Indian Ocean Territory', + code: '(IO)', + flag: '๐Ÿ‡ฎ๐Ÿ‡ด', + }, + { + name: 'Brunei Darussalam', + code: '(BN)', + flag: '๐Ÿ‡ง๐Ÿ‡ณ', + }, + { + name: 'Bulgaria', + code: '(BG)', + flag: '๐Ÿ‡ง๐Ÿ‡ฌ', + }, + { + name: 'Burkina Faso', + code: '(BF)', + flag: '๐Ÿ‡ง๐Ÿ‡ซ', + }, + { + name: 'Burundi', + code: '(BI)', + flag: '๐Ÿ‡ง๐Ÿ‡ฎ', + }, + ], + }, + { + title: 'C', + items: [ + { + name: 'Cambodia', + code: '(KH)', + flag: '๐Ÿ‡ฐ๐Ÿ‡ญ', + }, + { + name: 'Cameroon', + code: '(CM)', + flag: '๐Ÿ‡จ๐Ÿ‡ฒ', + }, + { + name: 'Canada', + code: '(CA)', + flag: '๐Ÿ‡จ๐Ÿ‡ฆ', + }, + { + name: 'Cape Verde', + code: '(CV)', + flag: '๐Ÿ‡จ๐Ÿ‡ป', + }, + { + name: 'Cayman Islands', + code: '(KY)', + flag: '๐Ÿ‡ฐ๐Ÿ‡พ', + }, + { + name: 'Central African Republic', + code: '(CF)', + flag: '๐Ÿ‡จ๐Ÿ‡ซ', + }, + { + name: 'Chad', + code: '(TD)', + flag: '๐Ÿ‡น๐Ÿ‡ฉ', + }, + { + name: 'Chile', + code: '(CL)', + flag: '๐Ÿ‡จ๐Ÿ‡ฑ', + }, + { + name: 'Christmas Island', + code: '(CX)', + flag: '๐Ÿ‡จ๐Ÿ‡ฝ', + }, + { + name: 'Cocos (Keeling) Islands', + code: '(CC)', + flag: '๐Ÿ‡จ๐Ÿ‡จ', + }, + { + name: 'Colombia', + code: '(CO)', + flag: '๐Ÿ‡จ๐Ÿ‡ด', + }, + { + name: 'Comoros', + code: '(KM)', + flag: '๐Ÿ‡ฐ๐Ÿ‡ฒ', + }, + { + name: 'Cook Islands', + code: '(CK)', + flag: '๐Ÿ‡จ๐Ÿ‡ฐ', + }, + { + name: 'Costa Rica', + code: '(CR)', + flag: '๐Ÿ‡จ๐Ÿ‡ท', + }, + { + name: "Cote d'Ivoire", + code: '(CI)', + flag: '๐Ÿ‡จ๐Ÿ‡ฎ', + }, + { + name: 'Croatia', + code: '(HR)', + flag: '๐Ÿ‡ญ๐Ÿ‡ท', + }, + { + name: 'Cuba', + code: '(CU)', + flag: '๐Ÿ‡จ๐Ÿ‡บ', + }, + { + name: 'Curaรงao', + code: '(CW)', + flag: '๐Ÿ‡จ๐Ÿ‡ผ', + }, + { + name: 'Cyprus', + code: '(CY)', + flag: '๐Ÿ‡จ๐Ÿ‡พ', + }, + { + name: 'Czech Republic', + code: '(CZ)', + flag: '๐Ÿ‡จ๐Ÿ‡ฟ', + }, + ], + }, + { + title: 'D', + items: [ + { + name: 'Democratic Republic of the Congo', + code: '(CD)', + flag: '๐Ÿ‡จ๐Ÿ‡ฉ', + }, + { + name: 'Denmark', + code: '(DK)', + flag: '๐Ÿ‡ฉ๐Ÿ‡ฐ', + }, + { + name: 'Djibouti', + code: '(DJ)', + flag: '๐Ÿ‡ฉ๐Ÿ‡ฏ', + }, + { + name: 'Dominica', + code: '(DM)', + flag: '๐Ÿ‡ฉ๐Ÿ‡ฒ', + }, + { + name: 'Dominican Republic', + code: '(DO)', + flag: '๐Ÿ‡ฉ๐Ÿ‡ด', + }, + ], + }, + { + title: 'E', + items: [ + { + name: 'Ecuador', + code: '(EC)', + flag: '๐Ÿ‡ช๐Ÿ‡จ', + }, + { + name: 'Egypt', + code: '(EG)', + flag: '๐Ÿ‡ช๐Ÿ‡ฌ', + }, + { + name: 'El Salvador', + code: '(SV)', + flag: '๐Ÿ‡ธ๐Ÿ‡ป', + }, + { + name: 'Equatorial Guinea', + code: '(GQ)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ถ', + }, + { + name: 'Eritrea', + code: '(ER)', + flag: '๐Ÿ‡ช๐Ÿ‡ท', + }, + { + name: 'Estonia', + code: '(EE)', + flag: '๐Ÿ‡ช๐Ÿ‡ช', + }, + { + name: 'Eswatini', + code: '(SZ)', + flag: '๐Ÿ‡ธ๐Ÿ‡ฟ', + }, + { + name: 'Ethiopia', + code: '(ET)', + flag: '๐Ÿ‡ช๐Ÿ‡น', + }, + ], + }, + { + title: 'F', + items: [ + { + name: 'Falkland Islands (Malvinas)', + code: '(FK)', + flag: '๐Ÿ‡ซ๐Ÿ‡ฐ', + }, + { + name: 'Faroe Islands', + code: '(FO)', + flag: '๐Ÿ‡ซ๐Ÿ‡ด', + }, + { + name: 'Fiji', + code: '(FJ)', + flag: '๐Ÿ‡ซ๐Ÿ‡ฏ', + }, + { + name: 'Finland', + code: '(FI)', + flag: '๐Ÿ‡ซ๐Ÿ‡ฎ', + }, + { + name: 'France', + code: '(FR)', + flag: '๐Ÿ‡ซ๐Ÿ‡ท', + }, + { + name: 'French Guiana', + code: '(GF)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ซ', + }, + { + name: 'French Polynesia', + code: '(PF)', + flag: '๐Ÿ‡ต๐Ÿ‡ซ', + }, + { + name: 'French Southern Territories', + code: '(TF)', + flag: '๐Ÿ‡น๐Ÿ‡ซ', + }, + ], + }, + { + title: 'G', + items: [ + { + name: 'Gabon', + code: '(GA)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ฆ', + }, + { + name: 'Georgia', + code: '(GE)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ช', + }, + { + name: 'Germany', + code: '(DE)', + flag: '๐Ÿ‡ฉ๐Ÿ‡ช', + }, + { + name: 'Ghana', + code: '(GH)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ญ', + }, + { + name: 'Gibraltar', + code: '(GI)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ฎ', + }, + { + name: 'Greece', + code: '(GR)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ท', + }, + { + name: 'Greenland', + code: '(GL)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ฑ', + }, + { + name: 'Grenada', + code: '(GD)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ฉ', + }, + { + name: 'Guadeloupe', + code: '(GP)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ต', + }, + { + name: 'Guam', + code: '(GU)', + flag: '๐Ÿ‡ฌ๐Ÿ‡บ', + }, + { + name: 'Guatemala', + code: '(GT)', + flag: '๐Ÿ‡ฌ๐Ÿ‡น', + }, + { + name: 'Guernsey', + code: '(GG)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ฌ', + }, + { + name: 'Guinea', + code: '(GN)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ณ', + }, + { + name: 'Guinea-Bissau', + code: '(GW)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ผ', + }, + { + name: 'Guyana', + code: '(GY)', + flag: '๐Ÿ‡ฌ๐Ÿ‡พ', + }, + ], + }, + { + title: 'H', + items: [ + { + name: 'Haiti', + code: '(HT)', + flag: '๐Ÿ‡ญ๐Ÿ‡น', + }, + { + name: 'Heard Island and McDonald Islands', + code: '(HM)', + flag: '๐Ÿ‡ญ๐Ÿ‡ฒ', + }, + { + name: 'Holy See (Vatican City State)', + code: '(VA)', + flag: '๐Ÿ‡ป๐Ÿ‡ฆ', + }, + { + name: 'Honduras', + code: '(HN)', + flag: '๐Ÿ‡ญ๐Ÿ‡ณ', + }, + { + name: 'Hong Kong', + code: '(HK)', + flag: '๐Ÿ‡ญ๐Ÿ‡ฐ', + }, + { + name: 'Hungary', + code: '(HU)', + flag: '๐Ÿ‡ญ๐Ÿ‡บ', + }, + ], + }, + { + title: 'I', + items: [ + { + name: 'Iceland', + code: '(IS)', + flag: '๐Ÿ‡ฎ๐Ÿ‡ธ', + }, + { + name: 'India', + code: '(IN)', + flag: '๐Ÿ‡ฎ๐Ÿ‡ณ', + }, + { + name: 'Indonesia', + code: '(ID)', + flag: '๐Ÿ‡ฎ๐Ÿ‡ฉ', + }, + { + name: 'Iraq', + code: '(IQ)', + flag: '๐Ÿ‡ฎ๐Ÿ‡ถ', + }, + { + name: 'Ireland', + code: '(IE)', + flag: '๐Ÿ‡ฎ๐Ÿ‡ช', + }, + { + name: 'Islamic Republic of Iran', + code: '(IR)', + flag: '๐Ÿ‡ฎ๐Ÿ‡ท', + }, + { + name: 'Isle of Man', + code: '(IM)', + flag: '๐Ÿ‡ฎ๐Ÿ‡ฒ', + }, + { + name: 'Israel', + code: '(IL)', + flag: '๐Ÿ‡ฎ๐Ÿ‡ฑ', + }, + { + name: 'Italy', + code: '(IT)', + flag: '๐Ÿ‡ฎ๐Ÿ‡น', + }, + ], + }, + { + title: 'J', + items: [ + { + name: 'Jamaica', + code: '(JM)', + flag: '๐Ÿ‡ฏ๐Ÿ‡ฒ', + }, + { + name: 'Japan', + code: '(JP)', + flag: '๐Ÿ‡ฏ๐Ÿ‡ต', + }, + { + name: 'Jersey', + code: '(JE)', + flag: '๐Ÿ‡ฏ๐Ÿ‡ช', + }, + { + name: 'Jordan', + code: '(JO)', + flag: '๐Ÿ‡ฏ๐Ÿ‡ด', + }, + ], + }, + { + title: 'K', + items: [ + { + name: 'Kazakhstan', + code: '(KZ)', + flag: '๐Ÿ‡ฐ๐Ÿ‡ฟ', + }, + { + name: 'Kenya', + code: '(KE)', + flag: '๐Ÿ‡ฐ๐Ÿ‡ช', + }, + { + name: 'Kiribati', + code: '(KI)', + flag: '๐Ÿ‡ฐ๐Ÿ‡ฎ', + }, + { + name: 'Kosovo', + code: '(XK)', + flag: '๐Ÿ‡ฝ๐Ÿ‡ฐ', + }, + { + name: 'Kuwait', + code: '(KW)', + flag: '๐Ÿ‡ฐ๐Ÿ‡ผ', + }, + { + name: 'Kyrgyzstan', + code: '(KG)', + flag: '๐Ÿ‡ฐ๐Ÿ‡ฌ', + }, + ], + }, + { + title: 'L', + items: [ + { + name: "Lao People's Democratic Republic", + code: '(LA)', + flag: '๐Ÿ‡ฑ๐Ÿ‡ฆ', + }, + { + name: 'Latvia', + code: '(LV)', + flag: '๐Ÿ‡ฑ๐Ÿ‡ป', + }, + { + name: 'Lebanon', + code: '(LB)', + flag: '๐Ÿ‡ฑ๐Ÿ‡ง', + }, + { + name: 'Lesotho', + code: '(LS)', + flag: '๐Ÿ‡ฑ๐Ÿ‡ธ', + }, + { + name: 'Liberia', + code: '(LR)', + flag: '๐Ÿ‡ฑ๐Ÿ‡ท', + }, + { + name: 'Libya', + code: '(LY)', + flag: '๐Ÿ‡ฑ๐Ÿ‡พ', + }, + { + name: 'Liechtenstein', + code: '(LI)', + flag: '๐Ÿ‡ฑ๐Ÿ‡ฎ', + }, + { + name: 'Lithuania', + code: '(LT)', + flag: '๐Ÿ‡ฑ๐Ÿ‡น', + }, + { + name: 'Luxembourg', + code: '(LU)', + flag: '๐Ÿ‡ฑ๐Ÿ‡บ', + }, + ], + }, + { + title: 'M', + items: [ + { + name: 'Macao', + code: '(MO)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ด', + }, + { + name: 'Madagascar', + code: '(MG)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ฌ', + }, + { + name: 'Malawi', + code: '(MW)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ผ', + }, + { + name: 'Malaysia', + code: '(MY)', + flag: '๐Ÿ‡ฒ๐Ÿ‡พ', + }, + { + name: 'Maldives', + code: '(MV)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ป', + }, + { + name: 'Mali', + code: '(ML)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ฑ', + }, + { + name: 'Malta', + code: '(MT)', + flag: '๐Ÿ‡ฒ๐Ÿ‡น', + }, + { + name: 'Marshall Islands', + code: '(MH)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ญ', + }, + { + name: 'Martinique', + code: '(MQ)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ถ', + }, + { + name: 'Mauritania', + code: '(MR)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ท', + }, + { + name: 'Mauritius', + code: '(MU)', + flag: '๐Ÿ‡ฒ๐Ÿ‡บ', + }, + { + name: 'Mayotte', + code: '(YT)', + flag: '๐Ÿ‡พ๐Ÿ‡น', + }, + { + name: 'Mexico', + code: '(MX)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ฝ', + }, + { + name: 'Micronesia, Federated States of', + code: '(FM)', + flag: '๐Ÿ‡ซ๐Ÿ‡ฒ', + }, + { + name: 'Moldova, Republic of', + code: '(MD)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ฉ', + }, + { + name: 'Monaco', + code: '(MC)', + flag: '๐Ÿ‡ฒ๐Ÿ‡จ', + }, + { + name: 'Mongolia', + code: '(MN)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ณ', + }, + { + name: 'Montenegro', + code: '(ME)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ช', + }, + { + name: 'Montserrat', + code: '(MS)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ธ', + }, + { + name: 'Morocco', + code: '(MA)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ฆ', + }, + { + name: 'Mozambique', + code: '(MZ)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ฟ', + }, + { + name: 'Myanmar', + code: '(MM)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ฒ', + }, + ], + }, + { + title: 'N', + items: [ + { + name: 'Namibia', + code: '(NA)', + flag: '๐Ÿ‡ณ๐Ÿ‡ฆ', + }, + { + name: 'Nauru', + code: '(NR)', + flag: '๐Ÿ‡ณ๐Ÿ‡ท', + }, + { + name: 'Nepal', + code: '(NP)', + flag: '๐Ÿ‡ณ๐Ÿ‡ต', + }, + { + name: 'Netherlands', + code: '(NL)', + flag: '๐Ÿ‡ณ๐Ÿ‡ฑ', + }, + { + name: 'New Caledonia', + code: '(NC)', + flag: '๐Ÿ‡ณ๐Ÿ‡จ', + }, + { + name: 'New Zealand', + code: '(NZ)', + flag: '๐Ÿ‡ณ๐Ÿ‡ฟ', + }, + { + name: 'Nicaragua', + code: '(NI)', + flag: '๐Ÿ‡ณ๐Ÿ‡ฎ', + }, + { + name: 'Niger', + code: '(NE)', + flag: '๐Ÿ‡ณ๐Ÿ‡ช', + }, + { + name: 'Nigeria', + code: '(NG)', + flag: '๐Ÿ‡ณ๐Ÿ‡ฌ', + }, + { + name: 'Niue', + code: '(NU)', + flag: '๐Ÿ‡ณ๐Ÿ‡บ', + }, + { + name: 'Norfolk Island', + code: '(NF)', + flag: '๐Ÿ‡ณ๐Ÿ‡ซ', + }, + { + name: 'North Korea', + code: '(KP)', + flag: '๐Ÿ‡ฐ๐Ÿ‡ต', + }, + { + name: 'Northern Mariana Islands', + code: '(MP)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ต', + }, + { + name: 'Norway', + code: '(NO)', + flag: '๐Ÿ‡ณ๐Ÿ‡ด', + }, + ], + }, + { + title: 'O', + items: [ + { + name: 'Oman', + code: '(OM)', + flag: '๐Ÿ‡ด๐Ÿ‡ฒ', + }, + ], + }, + { + title: 'P', + items: [ + { + name: 'Pakistan', + code: '(PK)', + flag: '๐Ÿ‡ต๐Ÿ‡ฐ', + }, + { + name: 'Palau', + code: '(PW)', + flag: '๐Ÿ‡ต๐Ÿ‡ผ', + }, + { + name: 'Panama', + code: '(PA)', + flag: '๐Ÿ‡ต๐Ÿ‡ฆ', + }, + { + name: 'Papua New Guinea', + code: '(PG)', + flag: '๐Ÿ‡ต๐Ÿ‡ฌ', + }, + { + name: 'Paraguay', + code: '(PY)', + flag: '๐Ÿ‡ต๐Ÿ‡พ', + }, + { + name: "People's Republic of China", + code: '(CN)', + flag: '๐Ÿ‡จ๐Ÿ‡ณ', + }, + { + name: 'Peru', + code: '(PE)', + flag: '๐Ÿ‡ต๐Ÿ‡ช', + }, + { + name: 'Philippines', + code: '(PH)', + flag: '๐Ÿ‡ต๐Ÿ‡ญ', + }, + { + name: 'Pitcairn', + code: '(PN)', + flag: '๐Ÿ‡ต๐Ÿ‡ณ', + }, + { + name: 'Poland', + code: '(PL)', + flag: '๐Ÿ‡ต๐Ÿ‡ฑ', + }, + { + name: 'Portugal', + code: '(PT)', + flag: '๐Ÿ‡ต๐Ÿ‡น', + }, + { + name: 'Puerto Rico', + code: '(PR)', + flag: '๐Ÿ‡ต๐Ÿ‡ท', + }, + ], + }, + { + title: 'Q', + items: [ + { + name: 'Qatar', + code: '(QA)', + flag: '๐Ÿ‡ถ๐Ÿ‡ฆ', + }, + ], + }, + { + title: 'R', + items: [ + { + name: 'Republic of the Congo', + code: '(CG)', + flag: '๐Ÿ‡จ๐Ÿ‡ฌ', + }, + { + name: 'Republic of The Gambia', + code: '(GM)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ฒ', + }, + { + name: 'Reunion', + code: '(RE)', + flag: '๐Ÿ‡ท๐Ÿ‡ช', + }, + { + name: 'Romania', + code: '(RO)', + flag: '๐Ÿ‡ท๐Ÿ‡ด', + }, + { + name: 'Russian Federation', + code: '(RU)', + flag: '๐Ÿ‡ท๐Ÿ‡บ', + }, + { + name: 'Rwanda', + code: '(RW)', + flag: '๐Ÿ‡ท๐Ÿ‡ผ', + }, + ], + }, + { + title: 'S', + items: [ + { + name: 'Saint Barthรฉlemy', + code: '(BL)', + flag: '๐Ÿ‡ง๐Ÿ‡ฑ', + }, + { + name: 'Saint Helena', + code: '(SH)', + flag: '๐Ÿ‡ธ๐Ÿ‡ญ', + }, + { + name: 'Saint Kitts and Nevis', + code: '(KN)', + flag: '๐Ÿ‡ฐ๐Ÿ‡ณ', + }, + { + name: 'Saint Lucia', + code: '(LC)', + flag: '๐Ÿ‡ฑ๐Ÿ‡จ', + }, + { + name: 'Saint Martin (French part)', + code: '(MF)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ซ', + }, + { + name: 'Saint Pierre and Miquelon', + code: '(PM)', + flag: '๐Ÿ‡ต๐Ÿ‡ฒ', + }, + { + name: 'Saint Vincent and the Grenadines', + code: '(VC)', + flag: '๐Ÿ‡ป๐Ÿ‡จ', + }, + { + name: 'Samoa', + code: '(WS)', + flag: '๐Ÿ‡ผ๐Ÿ‡ธ', + }, + { + name: 'San Marino', + code: '(SM)', + flag: '๐Ÿ‡ธ๐Ÿ‡ฒ', + }, + { + name: 'Sao Tome and Principe', + code: '(ST)', + flag: '๐Ÿ‡ธ๐Ÿ‡น', + }, + { + name: 'Saudi Arabia', + code: '(SA)', + flag: '๐Ÿ‡ธ๐Ÿ‡ฆ', + }, + { + name: 'Senegal', + code: '(SN)', + flag: '๐Ÿ‡ธ๐Ÿ‡ณ', + }, + { + name: 'Serbia', + code: '(RS)', + flag: '๐Ÿ‡ท๐Ÿ‡ธ', + }, + { + name: 'Seychelles', + code: '(SC)', + flag: '๐Ÿ‡ธ๐Ÿ‡จ', + }, + { + name: 'Sierra Leone', + code: '(SL)', + flag: '๐Ÿ‡ธ๐Ÿ‡ฑ', + }, + { + name: 'Singapore', + code: '(SG)', + flag: '๐Ÿ‡ธ๐Ÿ‡ฌ', + }, + { + name: 'Sint Maarten (Dutch part)', + code: '(SX)', + flag: '๐Ÿ‡ธ๐Ÿ‡ฝ', + }, + { + name: 'Slovakia', + code: '(SK)', + flag: '๐Ÿ‡ธ๐Ÿ‡ฐ', + }, + { + name: 'Slovenia', + code: '(SI)', + flag: '๐Ÿ‡ธ๐Ÿ‡ฎ', + }, + { + name: 'Solomon Islands', + code: '(SB)', + flag: '๐Ÿ‡ธ๐Ÿ‡ง', + }, + { + name: 'Somalia', + code: '(SO)', + flag: '๐Ÿ‡ธ๐Ÿ‡ด', + }, + { + name: 'South Africa', + code: '(ZA)', + flag: '๐Ÿ‡ฟ๐Ÿ‡ฆ', + }, + { + name: 'South Georgia and the South Sandwich Islands', + code: '(GS)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ธ', + }, + { + name: 'South Korea', + code: '(KR)', + flag: '๐Ÿ‡ฐ๐Ÿ‡ท', + }, + { + name: 'South Sudan', + code: '(SS)', + flag: '๐Ÿ‡ธ๐Ÿ‡ธ', + }, + { + name: 'Spain', + code: '(ES)', + flag: '๐Ÿ‡ช๐Ÿ‡ธ', + }, + { + name: 'Sri Lanka', + code: '(LK)', + flag: '๐Ÿ‡ฑ๐Ÿ‡ฐ', + }, + { + name: 'State of Palestine', + code: '(PS)', + flag: '๐Ÿ‡ต๐Ÿ‡ธ', + }, + { + name: 'Sudan', + code: '(SD)', + flag: '๐Ÿ‡ธ๐Ÿ‡ฉ', + }, + { + name: 'Suriname', + code: '(SR)', + flag: '๐Ÿ‡ธ๐Ÿ‡ท', + }, + { + name: 'Svalbard and Jan Mayen', + code: '(SJ)', + flag: '๐Ÿ‡ธ๐Ÿ‡ฏ', + }, + { + name: 'Sweden', + code: '(SE)', + flag: '๐Ÿ‡ธ๐Ÿ‡ช', + }, + { + name: 'Switzerland', + code: '(CH)', + flag: '๐Ÿ‡จ๐Ÿ‡ญ', + }, + { + name: 'Syrian Arab Republic', + code: '(SY)', + flag: '๐Ÿ‡ธ๐Ÿ‡พ', + }, + ], + }, + { + title: 'T', + items: [ + { + name: 'Taiwan, Province of China', + code: '(TW)', + flag: '๐Ÿ‡น๐Ÿ‡ผ', + }, + { + name: 'Tajikistan', + code: '(TJ)', + flag: '๐Ÿ‡น๐Ÿ‡ฏ', + }, + { + name: 'Thailand', + code: '(TH)', + flag: '๐Ÿ‡น๐Ÿ‡ญ', + }, + { + name: 'The Republic of North Macedonia', + code: '(MK)', + flag: '๐Ÿ‡ฒ๐Ÿ‡ฐ', + }, + { + name: 'Timor-Leste', + code: '(TL)', + flag: '๐Ÿ‡น๐Ÿ‡ฑ', + }, + { + name: 'Togo', + code: '(TG)', + flag: '๐Ÿ‡น๐Ÿ‡ฌ', + }, + { + name: 'Tokelau', + code: '(TK)', + flag: '๐Ÿ‡น๐Ÿ‡ฐ', + }, + { + name: 'Tonga', + code: '(TO)', + flag: '๐Ÿ‡น๐Ÿ‡ด', + }, + { + name: 'Trinidad and Tobago', + code: '(TT)', + flag: '๐Ÿ‡น๐Ÿ‡น', + }, + { + name: 'Tunisia', + code: '(TN)', + flag: '๐Ÿ‡น๐Ÿ‡ณ', + }, + { + name: 'Tรผrkiye', + code: '(TR)', + flag: '๐Ÿ‡น๐Ÿ‡ท', + }, + { + name: 'Turkmenistan', + code: '(TM)', + flag: '๐Ÿ‡น๐Ÿ‡ฒ', + }, + { + name: 'Turks and Caicos Islands', + code: '(TC)', + flag: '๐Ÿ‡น๐Ÿ‡จ', + }, + { + name: 'Tuvalu', + code: '(TV)', + flag: '๐Ÿ‡น๐Ÿ‡ป', + }, + ], + }, + { + title: 'U', + items: [ + { + name: 'Uganda', + code: '(UG)', + flag: '๐Ÿ‡บ๐Ÿ‡ฌ', + }, + { + name: 'Ukraine', + code: '(UA)', + flag: '๐Ÿ‡บ๐Ÿ‡ฆ', + }, + { + name: 'United Arab Emirates', + code: '(AE)', + flag: '๐Ÿ‡ฆ๐Ÿ‡ช', + }, + { + name: 'United Kingdom', + code: '(GB)', + flag: '๐Ÿ‡ฌ๐Ÿ‡ง', + }, + { + name: 'United Republic of Tanzania', + code: '(TZ)', + flag: '๐Ÿ‡น๐Ÿ‡ฟ', + }, + { + name: 'United States Minor Outlying Islands', + code: '(UM)', + flag: '๐Ÿ‡บ๐Ÿ‡ฒ', + }, + { + name: 'United States of America', + code: '(US)', + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }, + { + name: 'Uruguay', + code: '(UY)', + flag: '๐Ÿ‡บ๐Ÿ‡พ', + }, + { + name: 'Uzbekistan', + code: '(UZ)', + flag: '๐Ÿ‡บ๐Ÿ‡ฟ', + }, + ], + }, + { + title: 'V', + items: [ + { + name: 'Vanuatu', + code: '(VU)', + flag: '๐Ÿ‡ป๐Ÿ‡บ', + }, + { + name: 'Venezuela', + code: '(VE)', + flag: '๐Ÿ‡ป๐Ÿ‡ช', + }, + { + name: 'Vietnam', + code: '(VN)', + flag: '๐Ÿ‡ป๐Ÿ‡ณ', + }, + { + name: 'Virgin Islands, British', + code: '(VG)', + flag: '๐Ÿ‡ป๐Ÿ‡ฌ', + }, + { + name: 'Virgin Islands, U.S.', + code: '(VI)', + flag: '๐Ÿ‡ป๐Ÿ‡ฎ', + }, + ], + }, + { + title: 'W', + items: [ + { + name: 'Wallis and Futuna', + code: '(WF)', + flag: '๐Ÿ‡ผ๐Ÿ‡ซ', + }, + { + name: 'Western Sahara', + code: '(EH)', + flag: '๐Ÿ‡ช๐Ÿ‡ญ', + }, + ], + }, + { + title: 'Y', + items: [ + { + name: 'Yemen', + code: '(YE)', + flag: '๐Ÿ‡พ๐Ÿ‡ช', + }, + ], + }, + { + title: 'Z', + items: [ + { + name: 'Zambia', + code: '(ZM)', + flag: '๐Ÿ‡ฟ๐Ÿ‡ฒ', + }, + { + name: 'Zimbabwe', + code: '(ZW)', + flag: '๐Ÿ‡ฟ๐Ÿ‡ผ', + }, + ], + }, ]; selectItemTemplate(item: any, index: number, items: Array) { @@ -29,4 +1385,8 @@ export class ListPageModel extends Observable { okButtonText: DialogStrings.OK, }); } + + itemLoading(args: EventData): void { + (args.object as View).backgroundColor = 'transparent'; + } } diff --git a/apps/toolbox/src/pages/list-page.xml b/apps/toolbox/src/pages/list-page.xml index 1dfd79c25..67f1ecf72 100644 --- a/apps/toolbox/src/pages/list-page.xml +++ b/apps/toolbox/src/pages/list-page.xml @@ -1,18 +1,18 @@ - + - + + + + diff --git a/packages/core/ui/list-view/index.d.ts b/packages/core/ui/list-view/index.d.ts index c3ea1f050..b463e2239 100644 --- a/packages/core/ui/list-view/index.d.ts +++ b/packages/core/ui/list-view/index.d.ts @@ -99,6 +99,46 @@ export class ListView extends View { */ iosEstimatedRowHeight: CoreTypes.LengthType; + /** + * Gets or sets a value indicating whether the ListView should display sticky headers. + * When enabled, headers will remain visible at the top while scrolling through sections. + * + * @nsProperty + */ + stickyHeader: boolean; + + /** + * Gets or sets the template for sticky headers. + * + * @nsProperty + */ + stickyHeaderTemplate: string | Template; + + /** + * Gets or sets the height of sticky headers. + * + * @nsProperty + */ + stickyHeaderHeight: CoreTypes.LengthType; + + /** + * Gets or sets a value indicating whether the ListView should show default top padding above section headers. + * When set to false (default), removes iOS default spacing for a tighter layout. + * When set to true, preserves iOS default ~4-5px spacing above section headers. + * + * @nsProperty + */ + stickyHeaderTopPadding: boolean; + + /** + * Gets or sets a value indicating whether the ListView should treat items as sectioned data. + * When enabled, items array should contain objects with 'items' property for section content. + * Each section will have its own sticky header. + * + * @nsProperty + */ + sectioned: boolean; + /** * Forces the ListView to reload all its items. */ @@ -230,3 +270,28 @@ export const iosEstimatedRowHeightProperty: Property; + +/** + * Represents the observable property backing the stickyHeader property of each ListView instance. + */ +export const stickyHeaderProperty: Property; + +/** + * Represents the sticky header template property of each ListView instance. + */ +export const stickyHeaderTemplateProperty: Property; + +/** + * Represents the observable property backing the stickyHeaderHeight property of each ListView instance. + */ +export const stickyHeaderHeightProperty: Property; + +/** + * Represents the observable property backing the stickyHeaderTopPadding property of each ListView instance. + */ +export const stickyHeaderTopPaddingProperty: Property; + +/** + * Represents the observable property backing the sectioned property of each ListView instance. + */ +export const sectionedProperty: Property; diff --git a/packages/core/ui/list-view/index.ios.ts b/packages/core/ui/list-view/index.ios.ts index f5c6c8c93..eb066b623 100644 --- a/packages/core/ui/list-view/index.ios.ts +++ b/packages/core/ui/list-view/index.ios.ts @@ -1,7 +1,7 @@ import { ItemEventData } from '.'; -import { ListViewBase, separatorColorProperty, itemTemplatesProperty, iosEstimatedRowHeightProperty } from './list-view-common'; +import { ListViewBase, separatorColorProperty, itemTemplatesProperty, iosEstimatedRowHeightProperty, stickyHeaderProperty, stickyHeaderTemplateProperty, stickyHeaderHeightProperty, sectionedProperty } from './list-view-common'; import { CoreTypes } from '../../core-types'; -import { View, KeyedTemplate } from '../core/view'; +import { View, KeyedTemplate, Template } from '../core/view'; import { Length } from '../styling/length-shared'; import { Observable, EventData } from '../../data/observable'; import { Color } from '../../color'; @@ -10,6 +10,9 @@ import { StackLayout } from '../layouts/stack-layout'; import { ProxyViewContainer } from '../proxy-view-container'; import { profile } from '../../profiling'; import { Trace } from '../../trace'; +import { Builder } from '../builder'; +import { Label } from '../label'; +import { isFunction } from '../../utils/types'; export * from './list-view-common'; @@ -61,6 +64,41 @@ class ListViewCell extends UITableViewCell { public owner: WeakRef; } +@NativeClass +class ListViewHeaderCell extends UITableViewHeaderFooterView { + public static initWithEmptyBackground(): ListViewHeaderCell { + const cell = ListViewHeaderCell.new(); + // Clear background by default - this will make headers transparent + cell.backgroundColor = UIColor.clearColor; + + return cell; + } + + initWithReuseIdentifier(reuseIdentifier: string): this { + const cell = super.initWithReuseIdentifier(reuseIdentifier); + // Clear background by default - this will make headers transparent + cell.backgroundColor = UIColor.clearColor; + + return cell; + } + + public willMoveToSuperview(newSuperview: UIView): void { + const parent = (this.view ? this.view.parent : null); + + // When inside ListView and there is no newSuperview this header is + // removed from native visual tree so we remove it from our tree too. + if (parent && !newSuperview) { + parent._removeHeaderContainer(this); + } + } + + public get view(): View { + return this.owner ? this.owner.deref() : null; + } + + public owner: WeakRef; +} + function notifyForItemAtIndex(listView: ListViewBase, cell: any, view: View, eventName: string, indexPath: NSIndexPath) { const args = { eventName: eventName, @@ -88,10 +126,33 @@ class DataSource extends NSObject implements UITableViewDataSource { return dataSource; } + public numberOfSectionsInTableView(tableView: UITableView): number { + const owner = this._owner?.deref(); + + if (!owner) { + return 1; + } + + const sections = owner._getSectionCount(); + if (Trace.isEnabled()) { + Trace.write(`ListView: numberOfSections = ${sections} (sectioned: ${owner.sectioned})`, Trace.categories.Debug); + } + return sections; + } + public tableViewNumberOfRowsInSection(tableView: UITableView, section: number) { const owner = this._owner?.deref(); - return owner && owner.items ? owner.items.length : 0; + if (!owner) { + return 0; + } + + const sectionItems = owner._getItemsInSection(section); + const rowCount = sectionItems ? sectionItems.length : 0; + if (Trace.isEnabled()) { + Trace.write(`ListView: numberOfRows in section ${section} = ${rowCount}`, Trace.categories.Debug); + } + return rowCount; } public tableViewCellForRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): UITableViewCell { @@ -184,6 +245,55 @@ class UITableViewDelegateImpl extends NSObject implements UITableViewDelegate { return layout.toDeviceIndependentPixels(height); } + + public tableViewViewForHeaderInSection(tableView: UITableView, section: number): UIView { + const owner = this._owner?.deref(); + + if (!owner || !owner.stickyHeader || !owner.stickyHeaderTemplate) { + if (Trace.isEnabled()) { + Trace.write(`ListView: No sticky header (stickyHeader: ${owner?.stickyHeader}, hasTemplate: ${!!owner?.stickyHeaderTemplate})`, Trace.categories.Debug); + } + return null; + } + + if (Trace.isEnabled()) { + Trace.write(`ListView: Creating sticky header`, Trace.categories.Debug); + } + + const headerReuseIdentifier = 'stickyHeader'; + let headerCell = tableView.dequeueReusableHeaderFooterViewWithIdentifier(headerReuseIdentifier); + + if (!headerCell) { + // Use proper iOS initialization for registered header cells + headerCell = ListViewHeaderCell.alloc().initWithReuseIdentifier(headerReuseIdentifier); + headerCell.backgroundColor = UIColor.clearColor; + } + + owner._prepareHeader(headerCell, section); + + return headerCell; + } + + public tableViewHeightForHeaderInSection(tableView: UITableView, section: number): number { + const owner = this._owner?.deref(); + + if (!owner || !owner.stickyHeader) { + return 0; + } + + let height: number; + if (owner.stickyHeaderHeight === 'auto') { + height = 44; + } else { + height = layout.toDeviceIndependentPixels(Length.toDevicePixels(owner.stickyHeaderHeight, 44)); + } + + if (Trace.isEnabled()) { + Trace.write(`ListView: Sticky header height: ${height}`, Trace.categories.Debug); + } + + return height; + } } @NativeClass @@ -233,6 +343,54 @@ class UITableViewRowHeightDelegateImpl extends NSObject implements UITableViewDe return layout.toDeviceIndependentPixels(owner._effectiveRowHeight); } + + public tableViewViewForHeaderInSection(tableView: UITableView, section: number): UIView { + const owner = this._owner?.deref(); + + if (!owner || !owner.stickyHeader || !owner.stickyHeaderTemplate) { + if (Trace.isEnabled()) { + Trace.write(`ListView: No sticky header (stickyHeader: ${owner?.stickyHeader}, hasTemplate: ${!!owner?.stickyHeaderTemplate})`, Trace.categories.Debug); + } + return null; + } + + if (Trace.isEnabled()) { + Trace.write(`ListView: Creating sticky header`, Trace.categories.Debug); + } + + const headerReuseIdentifier = 'stickyHeader'; + let headerCell = tableView.dequeueReusableHeaderFooterViewWithIdentifier(headerReuseIdentifier); + + if (!headerCell) { + headerCell = ListViewHeaderCell.alloc().initWithReuseIdentifier(headerReuseIdentifier); + headerCell.backgroundColor = UIColor.clearColor; + } + + owner._prepareHeader(headerCell, section); + + return headerCell; + } + + public tableViewHeightForHeaderInSection(tableView: UITableView, section: number): number { + const owner = this._owner?.deref(); + + if (!owner || !owner.stickyHeader) { + return 0; + } + + let height: number; + if (owner.stickyHeaderHeight === 'auto') { + height = 44; + } else { + height = layout.toDeviceIndependentPixels(Length.toDevicePixels(owner.stickyHeaderHeight, 44)); + } + + if (Trace.isEnabled()) { + Trace.write(`ListView: Sticky header height: ${height}`, Trace.categories.Debug); + } + + return height; + } } export class ListView extends ListViewBase { @@ -244,11 +402,15 @@ export class ListView extends ListViewBase { private _preparingCell: boolean; private _isDataDirty: boolean; private _map: Map; + private _headerMap: Map; + private _preparingHeader: boolean; + private _headerTemplateCache: View; widthMeasureSpec = 0; constructor() { super(); this._map = new Map(); + this._headerMap = new Map(); this._heights = new Array(); } @@ -260,10 +422,20 @@ export class ListView extends ListViewBase { super.initNativeView(); const nativeView = this.nativeViewProtected; nativeView.registerClassForCellReuseIdentifier(ListViewCell.class(), this._defaultTemplate.key); + nativeView.registerClassForHeaderFooterViewReuseIdentifier(ListViewHeaderCell.class(), 'stickyHeader'); nativeView.estimatedRowHeight = DEFAULT_HEIGHT; nativeView.rowHeight = UITableViewAutomaticDimension; nativeView.dataSource = this._dataSource = DataSource.initWithOwner(new WeakRef(this)); this._delegate = UITableViewDelegateImpl.initWithOwner(new WeakRef(this)); + + // Control section header top padding (iOS 15+) + if (nativeView.respondsToSelector('setSectionHeaderTopPadding:')) { + if (!this.stickyHeaderTopPadding) { + nativeView.sectionHeaderTopPadding = 0; + } + // When stickyHeaderTopPadding is true, don't set the property to use iOS default + } + this._setNativeClipToBounds(); } @@ -296,13 +468,16 @@ export class ListView extends ListViewBase { } get _childrenCount(): number { - return this._map.size; + return this._map.size + this._headerMap.size; } public eachChildView(callback: (child: View) => boolean): void { this._map.forEach((view, key) => { callback(view); }); + this._headerMap.forEach((view, key) => { + callback(view); + }); } public scrollToIndex(index: number) { @@ -340,6 +515,11 @@ export class ListView extends ListViewBase { view.bindingContext = null; } }); + this._headerMap.forEach((view, nativeView, map) => { + if (!(view.bindingContext instanceof Observable)) { + view.bindingContext = null; + } + }); if (this.isLoaded) { this.nativeViewProtected.reloadData(); @@ -385,8 +565,8 @@ export class ListView extends ListViewBase { } public requestLayout(): void { - // When preparing cell don't call super - no need to invalidate our measure when cell desiredSize is changed. - if (!this._preparingCell) { + // When preparing cell or header don't call super - no need to invalidate our measure when cell/header desiredSize is changed. + if (!this._preparingCell && !this._preparingHeader) { super.requestLayout(); } } @@ -409,6 +589,9 @@ export class ListView extends ListViewBase { this._map.forEach((childView, listViewCell) => { View.measureChild(this, childView, childView._currentWidthMeasureSpec, childView._currentHeightMeasureSpec); }); + this._headerMap.forEach((childView, listViewHeaderCell) => { + View.measureChild(this, childView, childView._currentWidthMeasureSpec, childView._currentHeightMeasureSpec); + }); } public onLayout(left: number, top: number, right: number, bottom: number): void { @@ -423,6 +606,12 @@ export class ListView extends ListViewBase { View.layoutChild(this, childView, 0, 0, width, cellHeight); } }); + this._headerMap.forEach((childView, listViewHeaderCell) => { + const headerHeight = this.stickyHeaderHeight === 'auto' ? 44 : Length.toDevicePixels(this.stickyHeaderHeight, 44); + const width = layout.getMeasureSpecSize(this.widthMeasureSpec); + childView.iosOverflowSafeAreaEnabled = false; + View.layoutChild(this, childView, 0, 0, width, headerHeight); + }); } private _layoutCell(cellView: View, indexPath: NSIndexPath): number { @@ -445,11 +634,21 @@ export class ListView extends ListViewBase { this._preparingCell = true; let view: ItemView = cell.view; if (!view) { - view = this._getItemTemplate(indexPath.row).createView(); + if (this.sectioned) { + // For sectioned data, we need to calculate the absolute index for template selection + let absoluteIndex = 0; + for (let i = 0; i < indexPath.section; i++) { + absoluteIndex += this._getItemsInSection(i).length; + } + absoluteIndex += indexPath.row; + view = this._getItemTemplate(absoluteIndex).createView(); + } else { + view = this._getItemTemplate(indexPath.row).createView(); + } } const args = notifyForItemAtIndex(this, cell, view, ITEMLOADING, indexPath); - view = args.view || this._getDefaultItemContent(indexPath.row); + view = args.view || this._getDefaultItemContent(this.sectioned ? indexPath.row : indexPath.row); // Proxy containers should not get treated as layouts. // Wrap them in a real layout as well. @@ -469,8 +668,14 @@ export class ListView extends ListViewBase { cell.owner = new WeakRef(view); } - this._prepareItem(view, indexPath.row); - view._listViewItemIndex = indexPath.row; + if (this.sectioned) { + this._prepareItemInSection(view, indexPath.section, indexPath.row); + view._listViewItemIndex = indexPath.row; // Keep row index for compatibility + (view as any)._listViewSectionIndex = indexPath.section; + } else { + this._prepareItem(view, indexPath.row); + view._listViewItemIndex = indexPath.row; + } this._map.set(cell, view); // We expect that views returned from itemLoading are new (e.g. not reused). @@ -503,6 +708,163 @@ export class ListView extends ListViewBase { this._map.delete(cell); } + public _prepareHeader(headerCell: ListViewHeaderCell, section: number): number { + let headerHeight: number; + try { + this._preparingHeader = true; + let view: View = headerCell.view; + if (!view) { + view = this._getHeaderTemplate(); + if (!view) { + if (Trace.isEnabled()) { + Trace.write(`ListView: Failed to create header view for section ${section}`, Trace.categories.Debug); + } + // Create a fallback view + const lbl = new Label(); + lbl.text = `Section ${section}`; + view = lbl; + } + } + + // Handle header cell reuse + if (!headerCell.view) { + headerCell.owner = new WeakRef(view); + } else if (headerCell.view !== view) { + // Remove old view and set new one + (headerCell.view.nativeViewProtected)?.removeFromSuperview(); + this._removeHeaderContainer(headerCell); + headerCell.owner = new WeakRef(view); + } + + // Clear existing binding context and set new one + if (view.bindingContext) { + view.bindingContext = null; + } + + if (this.sectioned) { + const sectionData = this._getSectionData(section); + if (sectionData) { + view.bindingContext = sectionData; + } else { + // Fallback if section data is missing + view.bindingContext = { title: `Section ${section}`, section: section }; + } + } else { + view.bindingContext = this.bindingContext; + } + + // Force immediate binding context evaluation + if (view && typeof (view as any)._onBindingContextChanged === 'function') { + (view as any)._onBindingContextChanged(null, view.bindingContext); + + // Also trigger for child views + // @ts-ignore + if (view._childrenCount) { + view.eachChildView((child) => { + if (typeof (child as any)._onBindingContextChanged === 'function') { + (child as any)._onBindingContextChanged(null, view.bindingContext); + } + return true; + }); + } + } + this._headerMap.set(headerCell, view); + + // Add new header view to the cell + if (view && !view.parent) { + this._addView(view); + headerCell.contentView.addSubview(view.nativeViewProtected); + } + + // Request layout and measure/layout the header + if (view && view.bindingContext) { + view.requestLayout(); + } + + headerHeight = this._layoutHeader(view); + } finally { + this._preparingHeader = false; + } + + return headerHeight; + } + + private _layoutHeader(headerView: View): number { + if (headerView) { + const headerHeight = this.stickyHeaderHeight === 'auto' ? 44 : Length.toDevicePixels(this.stickyHeaderHeight, 44); + const heightMeasureSpec: number = layout.makeMeasureSpec(headerHeight, layout.EXACTLY); + + const measuredSize = View.measureChild(this, headerView, this.widthMeasureSpec, heightMeasureSpec); + // Layout the header with the measured size + View.layoutChild(this, headerView, 0, 0, measuredSize.measuredWidth, measuredSize.measuredHeight); + + return measuredSize.measuredHeight; + } + + return 44; + } + + private _getHeaderTemplate(): View { + if (this.stickyHeaderTemplate) { + if (__UI_USE_EXTERNAL_RENDERER__) { + if (isFunction(this.stickyHeaderTemplate)) { + return (