diff --git a/components/lib/calendar/Calendar.vue b/components/lib/calendar/Calendar.vue index 34c7143994..ef0bdb962c 100755 --- a/components/lib/calendar/Calendar.vue +++ b/components/lib/calendar/Calendar.vue @@ -2671,7 +2671,7 @@ export default { let innerHTML = ''; if (this.responsiveOptions) { - const comparer = new Intl.Collator(undefined, { numeric: true }).compare; + const comparer = ObjectUtils.localeComparator(); let responsiveOptions = [...this.responsiveOptions].filter((o) => !!(o.breakpoint && o.numMonths)).sort((o1, o2) => -1 * comparer(o1.breakpoint, o2.breakpoint)); for (let i = 0; i < responsiveOptions.length; i++) { diff --git a/components/lib/carousel/Carousel.vue b/components/lib/carousel/Carousel.vue index fb1685e811..cd189b170c 100755 --- a/components/lib/carousel/Carousel.vue +++ b/components/lib/carousel/Carousel.vue @@ -101,7 +101,7 @@ import ChevronLeftIcon from 'primevue/icons/chevronleft'; import ChevronRightIcon from 'primevue/icons/chevronright'; import ChevronUpIcon from 'primevue/icons/chevronup'; import Ripple from 'primevue/ripple'; -import { DomHandler, UniqueComponentId } from 'primevue/utils'; +import { DomHandler, UniqueComponentId, ObjectUtils } from 'primevue/utils'; import BaseCarousel from './BaseCarousel.vue'; export default { @@ -548,20 +548,13 @@ export default { if (this.responsiveOptions && !this.isUnstyled) { let _responsiveOptions = [...this.responsiveOptions]; - const comparer = new Intl.Collator(undefined, { numeric: true }).compare; + const comparer = ObjectUtils.localeComparator(); _responsiveOptions.sort((data1, data2) => { const value1 = data1.breakpoint; const value2 = data2.breakpoint; - let result = null; - if (value1 == null && value2 != null) result = -1; - else if (value1 != null && value2 == null) result = 1; - else if (value1 == null && value2 == null) result = 0; - else if (typeof value1 === 'string' && typeof value2 === 'string') result = comparer(value1, value2); - else result = value1 < value2 ? -1 : value1 > value2 ? 1 : 0; - - return -1 * result; + return ObjectUtils.sort(value1, value2, -1, comparer); }); for (let i = 0; i < _responsiveOptions.length; i++) { diff --git a/components/lib/datatable/BaseDataTable.vue b/components/lib/datatable/BaseDataTable.vue index 51015d0c7a..d307f759b6 100644 --- a/components/lib/datatable/BaseDataTable.vue +++ b/components/lib/datatable/BaseDataTable.vue @@ -78,6 +78,10 @@ export default { type: Number, default: 1 }, + nullSortOrder: { + type: Number, + default: 1 + }, multiSortMeta: { type: Array, default: null diff --git a/components/lib/datatable/DataTable.d.ts b/components/lib/datatable/DataTable.d.ts index 61275bdb54..84d063cc21 100755 --- a/components/lib/datatable/DataTable.d.ts +++ b/components/lib/datatable/DataTable.d.ts @@ -887,6 +887,11 @@ export interface DataTableProps { * Order to sort the data by default. */ sortOrder?: number | undefined; + /** + * Determines how null values are sorted. + * @defaultValue 1 + */ + nullSortOrder?: number; /** * Default sort order of an unsorted column. * @defaultValue 1 diff --git a/components/lib/datatable/DataTable.vue b/components/lib/datatable/DataTable.vue index 8ee20b53d7..931878a38b 100755 --- a/components/lib/datatable/DataTable.vue +++ b/components/lib/datatable/DataTable.vue @@ -341,6 +341,7 @@ export default { d_rows: this.rows, d_sortField: this.sortField, d_sortOrder: this.sortOrder, + d_nullSortOrder: this.nullSortOrder, d_multiSortMeta: this.multiSortMeta ? [...this.multiSortMeta] : [], d_groupRowsSortMeta: null, d_selectionKeys: null, @@ -381,6 +382,9 @@ export default { sortOrder(newValue) { this.d_sortOrder = newValue; }, + nullSortOrder(newValue) { + this.d_nullSortOrder = newValue; + }, multiSortMeta(newValue) { this.d_multiSortMeta = newValue; }, @@ -530,27 +534,19 @@ export default { } let data = [...value]; - let resolvedFieldDatas = new Map(); + let resolvedFieldData = new Map(); for (let item of data) { - resolvedFieldDatas.set(item, ObjectUtils.resolveFieldData(item, this.d_sortField)); + resolvedFieldData.set(item, ObjectUtils.resolveFieldData(item, this.d_sortField)); } - const comparer = new Intl.Collator(undefined, { numeric: true }).compare; + const comparer = ObjectUtils.localeComparator(); data.sort((data1, data2) => { - let value1 = resolvedFieldDatas.get(data1); - let value2 = resolvedFieldDatas.get(data2); - - let result = null; + let value1 = resolvedFieldData.get(data1); + let value2 = resolvedFieldData.get(data2); - if (value1 == null && value2 != null) result = -1; - else if (value1 != null && value2 == null) result = 1; - else if (value1 == null && value2 == null) result = 0; - else if (typeof value1 === 'string' && typeof value2 === 'string') result = comparer(value1, value2); - else result = value1 < value2 ? -1 : value1 > value2 ? 1 : 0; - - return this.d_sortOrder * result; + return ObjectUtils.sort(value1, value2, this.d_sortOrder, comparer, this.d_nullSortOrder); }); return data; @@ -579,23 +575,13 @@ export default { multisortField(data1, data2, index) { const value1 = ObjectUtils.resolveFieldData(data1, this.d_multiSortMeta[index].field); const value2 = ObjectUtils.resolveFieldData(data2, this.d_multiSortMeta[index].field); - let result = null; - - if (typeof value1 === 'string' || value1 instanceof String) { - if (value1.localeCompare && value1 !== value2) { - const comparer = new Intl.Collator(undefined, { numeric: true }).compare; - - return this.d_multiSortMeta[index].order * comparer(value1, value2); - } - } else { - result = value1 < value2 ? -1 : 1; - } + const comparer = ObjectUtils.localeComparator(); if (value1 === value2) { return this.d_multiSortMeta.length - 1 > index ? this.multisortField(data1, data2, index + 1) : 0; } - return this.d_multiSortMeta[index].order * result; + return ObjectUtils.sort(value1, value2, this.d_multiSortMeta[index].order, comparer, this.d_nullSortOrder); }, addMultiSortField(field) { let index = this.d_multiSortMeta.findIndex((meta) => meta.field === field); diff --git a/components/lib/dataview/DataView.vue b/components/lib/dataview/DataView.vue index 4cc5d8854b..b1e14fc3e1 100755 --- a/components/lib/dataview/DataView.vue +++ b/components/lib/dataview/DataView.vue @@ -110,20 +110,13 @@ export default { sort() { if (this.value) { const value = [...this.value]; - const comparer = new Intl.Collator(undefined, { numeric: true }).compare; + const comparer = ObjectUtils.localeComparator(); value.sort((data1, data2) => { let value1 = ObjectUtils.resolveFieldData(data1, this.sortField); let value2 = ObjectUtils.resolveFieldData(data2, this.sortField); - let result = null; - if (value1 == null && value2 != null) result = -1; - else if (value1 != null && value2 == null) result = 1; - else if (value1 == null && value2 == null) result = 0; - else if (typeof value1 === 'string' && typeof value2 === 'string') result = comparer(value1, value2); - else result = value1 < value2 ? -1 : value1 > value2 ? 1 : 0; - - return this.sortOrder * result; + return ObjectUtils.sort(value1, value2, this.sortOrder, comparer); }); return value; diff --git a/components/lib/galleria/GalleriaThumbnails.vue b/components/lib/galleria/GalleriaThumbnails.vue index acd89e8d08..5df896fad3 100755 --- a/components/lib/galleria/GalleriaThumbnails.vue +++ b/components/lib/galleria/GalleriaThumbnails.vue @@ -68,7 +68,7 @@ import ChevronLeftIcon from 'primevue/icons/chevronleft'; import ChevronRightIcon from 'primevue/icons/chevronright'; import ChevronUpIcon from 'primevue/icons/chevronup'; import Ripple from 'primevue/ripple'; -import { DomHandler } from 'primevue/utils'; +import { DomHandler, ObjectUtils } from 'primevue/utils'; export default { name: 'GalleriaThumbnails', @@ -438,20 +438,13 @@ export default { if (this.responsiveOptions && !this.isUnstyled) { this.sortedResponsiveOptions = [...this.responsiveOptions]; - const comparer = new Intl.Collator(undefined, { numeric: true }).compare; + const comparer = ObjectUtils.localeComparator(); this.sortedResponsiveOptions.sort((data1, data2) => { const value1 = data1.breakpoint; const value2 = data2.breakpoint; - let result = null; - if (value1 == null && value2 != null) result = -1; - else if (value1 != null && value2 == null) result = 1; - else if (value1 == null && value2 == null) result = 0; - else if (typeof value1 === 'string' && typeof value2 === 'string') result = comparer(value1, value2); - else result = value1 < value2 ? -1 : value1 > value2 ? 1 : 0; - - return -1 * result; + return ObjectUtils.sort(value1, value2, -1, comparer); }); for (let i = 0; i < this.sortedResponsiveOptions.length; i++) { diff --git a/components/lib/treetable/TreeTable.vue b/components/lib/treetable/TreeTable.vue index ca0dda4484..28f03c5c72 100755 --- a/components/lib/treetable/TreeTable.vue +++ b/components/lib/treetable/TreeTable.vue @@ -429,20 +429,13 @@ export default { }, sortNodesSingle(nodes) { let _nodes = [...nodes]; - const comparer = new Intl.Collator(undefined, { numeric: true }).compare; + const comparer = ObjectUtils.localeComparator(); _nodes.sort((node1, node2) => { const value1 = ObjectUtils.resolveFieldData(node1.data, this.d_sortField); const value2 = ObjectUtils.resolveFieldData(node2.data, this.d_sortField); - let result = null; - if (value1 == null && value2 != null) result = -1; - else if (value1 != null && value2 == null) result = 1; - else if (value1 == null && value2 == null) result = 0; - else if (typeof value1 === 'string' && typeof value2 === 'string') result = comparer(value1, value2); - else result = value1 < value2 ? -1 : value1 > value2 ? 1 : 0; - - return this.d_sortOrder * result; + return ObjectUtils.sort(value1, value2, this.d_sortOrder, comparer); }); return _nodes; @@ -462,22 +455,13 @@ export default { multisortField(node1, node2, index) { const value1 = ObjectUtils.resolveFieldData(node1.data, this.d_multiSortMeta[index].field); const value2 = ObjectUtils.resolveFieldData(node2.data, this.d_multiSortMeta[index].field); - let result = null; - - if (value1 == null && value2 != null) result = -1; - else if (value1 != null && value2 == null) result = 1; - else if (value1 == null && value2 == null) result = 0; - else { - if (value1 === value2) { - return this.d_multiSortMeta.length - 1 > index ? this.multisortField(node1, node2, index + 1) : 0; - } else { - if ((typeof value1 === 'string' || value1 instanceof String) && (typeof value2 === 'string' || value2 instanceof String)) - return this.d_multiSortMeta[index].order * new Intl.Collator(undefined, { numeric: true }).compare(value1, value2); - else result = value1 < value2 ? -1 : 1; - } + const comparer = ObjectUtils.localeComparator(); + + if (value1 === value2) { + return this.d_multiSortMeta.length - 1 > index ? this.multisortField(node1, node2, index + 1) : 0; } - return this.d_multiSortMeta[index].order * result; + return ObjectUtils.sort(value1, value2, this.d_multiSortMeta[index].order, comparer); }, filter(value) { let filteredNodes = []; diff --git a/components/lib/utils/ObjectUtils.js b/components/lib/utils/ObjectUtils.js index e5e31629ab..4f95ab537c 100755 --- a/components/lib/utils/ObjectUtils.js +++ b/components/lib/utils/ObjectUtils.js @@ -302,6 +302,37 @@ export default { return index; }, + sort(value1, value2, order = 1, comparator, nullSortOrder = 1) { + const result = this.compare(value1, value2, comparator, order); + let finalSortOrder = order; + + // nullSortOrder == 1 means Excel like sort nulls at bottom + if (this.isEmpty(value1) || this.isEmpty(value2)) { + finalSortOrder = nullSortOrder === 1 ? order : nullSortOrder; + } + + return finalSortOrder * result; + }, + + compare(value1, value2, comparator, order = 1) { + let result = -1; + const emptyValue1 = this.isEmpty(value1); + const emptyValue2 = this.isEmpty(value2); + + if (emptyValue1 && emptyValue2) result = 0; + else if (emptyValue1) result = order; + else if (emptyValue2) result = -order; + else if (typeof value1 === 'string' && typeof value2 === 'string') result = comparator(value1, value2); + else result = value1 < value2 ? -1 : value1 > value2 ? 1 : 0; + + return result; + }, + + localeComparator() { + //performance gain using Int.Collator. It is not recommended to use localeCompare against large arrays. + return new Intl.Collator(undefined, { numeric: true }).compare; + }, + nestedKeys(obj = {}, parentKey = '') { return Object.entries(obj).reduce((o, [key, value]) => { const currentKey = parentKey ? `${parentKey}.${key}` : key; diff --git a/components/lib/utils/Utils.d.ts b/components/lib/utils/Utils.d.ts index 2917f74e80..96daafc7d3 100644 --- a/components/lib/utils/Utils.d.ts +++ b/components/lib/utils/Utils.d.ts @@ -89,6 +89,8 @@ export declare class ObjectUtils { static isPrintableCharacter(char: string): boolean; static findLast(value: any[], callback: () => any): any; static findLastIndex(value: any[], callback: () => any): number; + static sort(value1: any, value2: any, order: number, comparator: (a: any, b: any) => any, nullSortOrder: number): number; + static compare(value1: any, value2: any, comparator: (a: any, b: any) => any, order: number): number; static nestedKeys(obj: object, parentKey?: string): string[]; } diff --git a/doc/common/apidoc/index.json b/doc/common/apidoc/index.json index be7c7de136..eb962cc7ab 100644 --- a/doc/common/apidoc/index.json +++ b/doc/common/apidoc/index.json @@ -18854,6 +18854,14 @@ "default": "", "description": "Order to sort the data by default." }, + { + "name": "nullSortOrder", + "optional": true, + "readonly": false, + "type": "number", + "default": "1", + "description": "Determines how null values are sorted." + }, { "name": "defaultSortOrder", "optional": true,