diff --git a/packages/demo/src/app-routing.ts b/packages/demo/src/app-routing.ts index 0549c194..b86f8d3d 100644 --- a/packages/demo/src/app-routing.ts +++ b/packages/demo/src/app-routing.ts @@ -69,6 +69,7 @@ import Options37 from './options/options37.js'; import Options38 from './options/options38.js'; import Options39 from './options/options39.js'; import Options40 from './options/options40.js'; +import Options41 from './options/options41.js'; export const navbarRouting = [ { name: 'getting-started', view: '/src/getting-started.html', viewModel: GettingStarted, title: 'Getting Started' }, @@ -139,7 +140,8 @@ export const exampleRouting = [ { name: 'options37', view: '/src/options/options37.html', viewModel: Options37, title: 'Navigation Highlight' }, { name: 'options38', view: '/src/options/options38.html', viewModel: Options38, title: 'Dark Mode' }, { name: 'options39', view: '/src/options/options39.html', viewModel: Options39, title: 'Label Id (aria-labelledby)' }, - { name: 'options40', view: '/src/options/options40.html', viewModel: Options40, title: 'Filter Data' }, + { name: 'options40', view: '/src/options/options40.html', viewModel: Options40, title: 'Pre-Filter Data' }, + { name: 'options41', view: '/src/options/options41.html', viewModel: Options41, title: 'Pre-Sort Data' }, ], }, { diff --git a/packages/demo/src/options/options40.html b/packages/demo/src/options/options40.html index f325bf7a..c17c35b4 100644 --- a/packages/demo/src/options/options40.html +++ b/packages/demo/src/options/options40.html @@ -1,7 +1,7 @@

- Filter Data + Pre-Filter Data Code @@ -17,7 +17,7 @@

-
Use preFilter(predicate) to pre-filter the data collection before rendering the select dropdown in the UI.
+
Use preFilter: predicate to pre-filter the data collection before rendering the select dropdown in the UI.

diff --git a/packages/demo/src/options/options41.html b/packages/demo/src/options/options41.html new file mode 100644 index 00000000..18c2dc2b --- /dev/null +++ b/packages/demo/src/options/options41.html @@ -0,0 +1,87 @@ +
+
+

+ Pre-Sort Data + + Code + + html + | + ts + + +

+
Use preSort: comparer to pre-sort the data collection before rendering the select dropdown in the UI.
+
+
+ +
+
+ + +
+ +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+
+
diff --git a/packages/demo/src/options/options41.ts b/packages/demo/src/options/options41.ts new file mode 100644 index 00000000..ffb70ddc --- /dev/null +++ b/packages/demo/src/options/options41.ts @@ -0,0 +1,153 @@ +import { type MultipleSelectInstance, type OptGroupRowData, multipleSelect } from 'multiple-select-vanilla'; + +export default class Example { + ms1?: MultipleSelectInstance; + ms2?: MultipleSelectInstance; + ms3?: MultipleSelectInstance; + ms4?: MultipleSelectInstance; + + mount() { + this.ms1 = multipleSelect('select[data-test=select1]', { + filter: true, + preSort: (item1, item2) => { + // sort by value in reverse order + const direction = -1; // reverse order + return (+item1.value! - +item2.value!) * direction; + }, + }) as MultipleSelectInstance; + + this.ms2 = multipleSelect('select[data-test=select2]', { + filter: true, + preSort: (item1, item2) => { + // sort by value in reverse order + const direction = -1; // reverse order + // @ts-ignore + if (direction === 1) { + return (item1 as OptGroupRowData).label < (item2 as OptGroupRowData).label ? -1 : 1; + } + return (item1 as OptGroupRowData).label < (item2 as OptGroupRowData).label ? 1 : -1; + }, + }) as MultipleSelectInstance; + + this.ms3 = multipleSelect('select[data-test=select3]', { + data: { + 1: 'January', + 2: 'February', + 3: 'March', + 4: 'April', + 5: 'May', + 6: 'June', + 7: 'July', + 8: 'August', + 9: 'September', + 10: 'October', + 11: 'November', + 12: 'December', + }, + preSort: (item1, item2) => { + // sort by value in reverse order + const direction = -1; // reverse order + return (+item1.value! - +item2.value!) * direction; + }, + }) as MultipleSelectInstance; + + this.ms4 = multipleSelect('select[data-test=select4]', { + data: [ + { + type: 'optgroup', + label: 'Q1', + children: [ + { + text: 'January', + value: 1, + selected: true, + }, + { + text: 'February', + value: 2, + }, + { + text: 'March', + value: 3, + }, + ], + }, + { + type: 'optgroup', + label: 'Q2', + children: [ + { + text: 'April', + value: 4, + }, + { + text: 'May', + value: 5, + }, + { + text: 'June', + value: 6, + }, + ], + }, + { + type: 'optgroup', + label: 'Q3', + children: [ + { + text: 'July', + value: 7, + }, + { + text: 'August', + value: 8, + }, + { + text: 'September', + value: 9, + }, + ], + }, + { + type: 'optgroup', + label: 'Q4', + children: [ + { + text: 'October', + value: 10, + }, + { + text: 'November', + value: 11, + }, + { + text: 'December', + value: 12, + }, + ], + }, + ], + preSort: (item1, item2) => { + // sort by value in reverse order + const direction = -1; // reverse order + // @ts-ignore + if (direction === 1) { + return (item1 as OptGroupRowData).label < (item2 as OptGroupRowData).label ? -1 : 1; + } + return (item1 as OptGroupRowData).label < (item2 as OptGroupRowData).label ? 1 : -1; + }, + }) as MultipleSelectInstance; + } + + unmount() { + // destroy ms instance(s) to avoid DOM leaks + this.ms1?.destroy(); + this.ms2?.destroy(); + this.ms3?.destroy(); + this.ms4?.destroy(); + this.ms1 = undefined; + this.ms2 = undefined; + this.ms3 = undefined; + this.ms4 = undefined; + } +} diff --git a/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts b/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts index d449976e..e23b8393 100644 --- a/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts +++ b/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts @@ -316,11 +316,16 @@ export class MultipleSelectInstance { this.fromHtml = true; } - // user might filter its data prior to rendering the list + // user might pre-filter its data prior to rendering the list if (this.data && this.options.preFilter) { this.data = this.data.filter(this.options.preFilter); } + // user might pre-sort its data prior to rendering the list + if (this.data && this.options.preSort) { + this.data = this.data.sort(this.options.preSort); + } + this.dataTotal = setDataKeys(this.data || []); } diff --git a/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts b/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts index 50425612..4233228f 100644 --- a/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts +++ b/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts @@ -169,9 +169,18 @@ export interface MultipleSelectOption extends MultipleSelectLocale { /** Defines the position of select dropdown, can only be bottom or top. By default this option is set to bottom. */ position: 'bottom' | 'top'; - /** Custom Filter predicate function to pre-filter the data collection before rendering the select dropdown. Executed before `preSort`. */ + /** + * Custom Filter predicate function callback to pre-filter the data collection before rendering the select dropdown. + * Note that this is executed before `preSort` (when defined). + */ preFilter?: (dataItem: OptionRowData | OptGroupRowData) => boolean; + /** + * Custom Sort Comparer function callback to pre-sort the data collection before rendering the select dropdown. + * Note that this is executed after `preSort` (when defined). + */ + preSort?: (item1: OptionRowData | OptGroupRowData, item2: OptionRowData | OptGroupRowData) => number; + /** Defaults to False, should we render option labels as html? */ renderOptionLabelAsHtml?: boolean; diff --git a/playwright/e2e/options40.spec.ts b/playwright/e2e/options40.spec.ts index 2b449230..119fc533 100644 --- a/playwright/e2e/options40.spec.ts +++ b/playwright/e2e/options40.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test'; -test.describe('Options 40 - Filter Data', () => { - test('all select dropdown should have data filtered out', async ({ page }) => { +test.describe('Options 40 - Pre-Filter Data', () => { + test('all select dropdown should have data pre-filtered', async ({ page }) => { await page.goto('#/options40'); // 1st select @@ -35,7 +35,7 @@ test.describe('Options 40 - Filter Data', () => { await expect(await page.getByRole('option', { name: 'March' })).toHaveCount(1); await expect(await page.getByRole('option', { name: 'April' })).toHaveCount(0); await expect(await page.getByRole('option', { name: 'May' })).toHaveCount(1); - const select3LiElms = await page.locator('div[data-test=select1] li[role="option"]'); + const select3LiElms = await page.locator('div[data-test=select3] li[role="option"]'); await expect(select3LiElms).toHaveCount(10); await page.locator('div[data-test=select3].ms-parent').click(); diff --git a/playwright/e2e/options41.spec.ts b/playwright/e2e/options41.spec.ts new file mode 100644 index 00000000..dc7014fb --- /dev/null +++ b/playwright/e2e/options41.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Options 41 - Pre-Sort Data', () => { + test('all select dropdown should have data pre-filtered', async ({ page }) => { + await page.goto('#/options41'); + + // 1st select + await page.locator('div[data-test=select1].ms-parent').click(); + const select1LiElms = await page.locator('div[data-test=select1] li[role="option"]'); + await expect(select1LiElms).toHaveCount(12); + const select1li1 = await page.locator('div[data-test=select1] .ms-drop li').nth(0); + await expect(await select1li1.locator('label')).toHaveText('December'); + const select1li2 = await page.locator('div[data-test=select1] .ms-drop li').nth(1); + await expect(await select1li2.locator('label')).toHaveText('November'); + const select1li3 = await page.locator('div[data-test=select1] .ms-drop li').nth(2); + await expect(await select1li3.locator('label')).toHaveText('October'); + await page.locator('div[data-test=select1].ms-parent').click(); + + // 2nd select + await page.locator('div[data-test=select2].ms-parent').click(); + const select2GroupElms = await page.locator('div[data-test=select2] li.group'); + const select2li1 = await page.locator('div[data-test=select2] .ms-drop li.group').nth(0); + await expect(await select2li1.locator('label.optgroup')).toHaveText('Group 3'); + const select2li2 = await page.locator('div[data-test=select2] .ms-drop li.group').nth(1); + await expect(await select2li2.locator('label.optgroup')).toHaveText('Group 2'); + await expect(select2GroupElms).toHaveCount(3); + await page.locator('div[data-test=select2].ms-parent').click(); + + // 3rd select + await page.locator('div[data-test=select3].ms-parent').click(); + const select3LiElms = await page.locator('div[data-test=select3] li[role="option"]'); + await expect(select3LiElms).toHaveCount(12); + const select3li1 = await page.locator('div[data-test=select3] .ms-drop li').nth(0); + await expect(await select3li1.locator('label')).toHaveText('December'); + const select3li2 = await page.locator('div[data-test=select3] .ms-drop li').nth(1); + await expect(await select3li2.locator('label')).toHaveText('November'); + const select3li3 = await page.locator('div[data-test=select3] .ms-drop li').nth(2); + await expect(await select3li3.locator('label')).toHaveText('October'); + await page.locator('div[data-test=select3].ms-parent').click(); + + // 4th select + await page.locator('div[data-test=select4].ms-parent').click(); + const select4GroupElms = await page.locator('div[data-test=select4] li.group'); + const select4li1 = await page.locator('div[data-test=select4] .ms-drop li.group').nth(0); + await expect(await select4li1.locator('label.optgroup')).toHaveText('Q4'); + const select4li2 = await page.locator('div[data-test=select4] .ms-drop li.group').nth(1); + await expect(await select4li2.locator('label.optgroup')).toHaveText('Q3'); + await expect(select4GroupElms).toHaveCount(4); + await page.locator('div[data-test=select4].ms-parent').click(); + }); +});