diff --git a/package.json b/package.json index b933f45ab..f1de0892a 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,8 @@ "build:demo": "webpack --progress -p --env.production --env.extractCss" }, "dependencies": { - "@slickgrid-universal/common": "^0.14.0", - "@slickgrid-universal/empty-warning-component": "^0.14.0", + "@slickgrid-universal/common": "^0.14.1", + "@slickgrid-universal/empty-warning-component": "^0.14.1", "aurelia-event-aggregator": "^1.0.3", "aurelia-framework": "^1.3.1", "aurelia-i18n": "^3.1.4", @@ -70,12 +70,12 @@ "i18next": ">= 14.1.1" }, "devDependencies": { - "@slickgrid-universal/composite-editor-component": "^0.14.0", - "@slickgrid-universal/excel-export": "^0.14.0", - "@slickgrid-universal/graphql": "^0.14.0", - "@slickgrid-universal/odata": "^0.14.0", - "@slickgrid-universal/rxjs-observable": "^0.14.0", - "@slickgrid-universal/text-export": "^0.14.0", + "@slickgrid-universal/composite-editor-component": "^0.14.1", + "@slickgrid-universal/excel-export": "^0.14.1", + "@slickgrid-universal/graphql": "^0.14.1", + "@slickgrid-universal/odata": "^0.14.1", + "@slickgrid-universal/rxjs-observable": "^0.14.1", + "@slickgrid-universal/text-export": "^0.14.1", "@types/bluebird": "^3.5.35", "@types/dompurify": "^2.2.2", "@types/i18next-xhr-backend": "^1.4.2", diff --git a/src/aurelia-slickgrid/constants.ts b/src/aurelia-slickgrid/constants.ts index 8cc5db439..e97bd01fd 100644 --- a/src/aurelia-slickgrid/constants.ts +++ b/src/aurelia-slickgrid/constants.ts @@ -94,6 +94,7 @@ export class Constants { 'onBeforeExportToTextFile', 'onAfterExportToTextFile', 'onGridStateChanged', + 'onHeaderMenuColumnResizeByContent', 'onPaginationChanged', 'onItemAdded', 'onItemDeleted', diff --git a/src/aurelia-slickgrid/custom-elements/__tests__/aurelia-slickgrid.spec.ts b/src/aurelia-slickgrid/custom-elements/__tests__/aurelia-slickgrid.spec.ts index a5196e4a7..a3cec6cfd 100644 --- a/src/aurelia-slickgrid/custom-elements/__tests__/aurelia-slickgrid.spec.ts +++ b/src/aurelia-slickgrid/custom-elements/__tests__/aurelia-slickgrid.spec.ts @@ -1,5 +1,5 @@ -import { EventAggregator } from 'aurelia-event-aggregator'; import 'jest-extended'; +import { EventAggregator } from 'aurelia-event-aggregator'; import { BindingEngine, Container } from 'aurelia-framework'; import { of, throwError } from 'rxjs'; import { @@ -32,13 +32,11 @@ import { SortService, TreeDataService } from '@slickgrid-universal/common'; -import * as aureliaSlickgridUtilities from '../aurelia-slickgrid-utilities'; import { SlickEmptyWarningComponent } from '@slickgrid-universal/empty-warning-component'; import { GraphqlPaginatedResult, GraphqlService, GraphqlServiceApi, GraphqlServiceOption } from '@slickgrid-universal/graphql'; import { TextExportService } from '@slickgrid-universal/text-export'; - import { RxJsResourceStub } from '../../../../test/rxjsResourceStub'; import { HttpStub } from '../../../../test/httpClientStub'; import { MockSlickEvent, MockSlickEventHandler } from '../../../../test/mockSlickEvent'; @@ -47,6 +45,7 @@ import { AureliaUtilService, ContainerService, TranslaterService } from '../../s import { PubSubService } from '../../services/pubSub.service'; import { ResizerService } from '../../services/resizer.service'; import { AureliaSlickgridCustomElement } from '../aurelia-slickgrid'; +import * as aureliaSlickgridUtilities from '../aurelia-slickgrid-utilities'; const mockAutoAddCustomEditorFormatter = jest.fn(); @@ -123,6 +122,7 @@ const filterServiceStub = { bindLocalOnSort: jest.fn(), bindBackendOnSort: jest.fn(), populateColumnFilterSearchTermPresets: jest.fn(), + refreshTreeDataFilters: jest.fn(), getColumnFilters: jest.fn(), } as unknown as FilterService; @@ -180,10 +180,11 @@ const sortServiceStub = { const treeDataServiceStub = { init: jest.fn(), - convertFlatDatasetConvertToHierarhicalView: jest.fn(), - initializeHierarchicalDataset: jest.fn(), + convertFlatParentChildToTreeDataset: jest.fn(), + convertFlatParentChildToTreeDatasetAndSort: jest.fn(), dispose: jest.fn(), handleOnCellClick: jest.fn(), + sortHierarchicalDataset: jest.fn(), toggleTreeDataCollapse: jest.fn(), } as unknown as TreeDataService; @@ -1822,6 +1823,10 @@ describe('Aurelia-Slickgrid Component instantiated via Constructor', () => { }); describe('Custom Footer', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should have a Custom Footer when "showCustomFooter" is enabled and there are no Pagination used', (done) => { const mockColDefs = [{ id: 'name', field: 'name', editor: undefined, internalColumnEditor: {} }]; const mockGridOptions = { enableTranslate: true, showCustomFooter: true, }; @@ -1938,10 +1943,10 @@ describe('Aurelia-Slickgrid Component instantiated via Constructor', () => { const expectation = { startTime: expect.toBeDate(), endTime: expect.toBeDate(), - itemCount: 0, + itemCount: 2, totalItemCount: 0 }; - jest.spyOn(mockDataView, 'getLength').mockReturnValue(0); + jest.spyOn(mockDataView, 'getLength').mockReturnValue(2); customElement.gridOptions = { enablePagination: false, showCustomFooter: true }; customElement.initialization(slickEventHandler); @@ -2094,57 +2099,114 @@ describe('Aurelia-Slickgrid Component instantiated via Constructor', () => { }); describe('Tree Data View', () => { - it('should throw an error when enableTreeData is enabled with Pagination since that is not supported', (done) => { - try { - customElement.gridOptions = { enableTreeData: true, enablePagination: true } as GridOption; - customElement.initialization(slickEventHandler); - } catch (e) { - expect(e.toString()).toContain('[Aurelia-Slickgrid] It looks like you are trying to use Tree Data with Pagination but unfortunately that is simply not supported because of its complexity.'); - customElement.dispose(); - done(); - } + afterEach(() => { + customElement.dispose(); + jest.clearAllMocks(); }); - it('should throw an error when enableTreeData is enabled without passing a "columnId"', (done) => { - try { - customElement.gridOptions = { enableTreeData: true, treeDataOptions: {} } as unknown as GridOption; - customElement.initialization(slickEventHandler); + it('should change flat dataset and expect "convertFlatParentChildToTreeDatasetAndSort" being called with other methods', () => { + const mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', parentId: 0 }]; + const mockHierarchical = [{ id: 0, file: 'documents', files: [{ id: 1, file: 'vacation.txt' }] }]; + const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set'); + const treeConvertAndSortSpy = jest.spyOn(treeDataServiceStub, 'convertFlatParentChildToTreeDatasetAndSort').mockReturnValue({ hierarchical: mockHierarchical as any[], flat: mockFlatDataset as any[] }); + const refreshTreeSpy = jest.spyOn(filterServiceStub, 'refreshTreeDataFilters'); - } catch (e) { - expect(e.toString()).toContain('[Aurelia-Slickgrid] When enabling tree data, you must also provide the "treeDataOption" property in your Grid Options with "childrenPropName" or "parentPropName"'); - customElement.dispose(); - done(); - } + customElement.gridOptions = { + enableTreeData: true, treeDataOptions: { + columnId: 'file', parentPropName: 'parentId', childrenPropName: 'files', + initialSort: { columndId: 'file', direction: 'ASC' } + } + } as unknown as GridOption; + customElement.initialization(slickEventHandler); + customElement.dataset = mockFlatDataset; + customElement.datasetChanged(mockFlatDataset, []); + + expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical); + expect(refreshTreeSpy).toHaveBeenCalled(); + expect(treeConvertAndSortSpy).toHaveBeenCalled(); }); - it('should change flat dataset and expect being called with other methods', () => { + it('should change flat dataset and expect "convertFlatParentChildToTreeDataset" being called (without sorting) and other methods as well', () => { const mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', parentId: 0 }]; const mockHierarchical = [{ id: 0, file: 'documents', files: [{ id: 1, file: 'vacation.txt' }] }]; const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set'); - jest.spyOn(treeDataServiceStub, 'initializeHierarchicalDataset').mockReturnValue({ hierarchical: mockHierarchical, flat: mockFlatDataset }); + const treeConvertSpy = jest.spyOn(treeDataServiceStub, 'convertFlatParentChildToTreeDataset').mockReturnValue(mockHierarchical as any[]); + const refreshTreeSpy = jest.spyOn(filterServiceStub, 'refreshTreeDataFilters'); - customElement.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file', parentPropName: 'parentId', childrenPropName: 'files' } } as unknown as GridOption; + customElement.gridOptions = { + enableTreeData: true, treeDataOptions: { + columnId: 'file', parentPropName: 'parentId', childrenPropName: 'files' + } + } as unknown as GridOption; customElement.initialization(slickEventHandler); + customElement.dataset = mockFlatDataset; customElement.datasetChanged(mockFlatDataset, []); expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical); + expect(refreshTreeSpy).toHaveBeenCalled(); + expect(treeConvertSpy).toHaveBeenCalled(); }); - it('should change hierarchical dataset and expect processTreeDataInitialSort being called with other methods', () => { + it('should change hierarchical dataset and expect processTreeDataInitialSort being called with other methods', (done) => { const mockHierarchical = [{ file: 'documents', files: [{ file: 'vacation.txt' }] }]; const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set'); const clearFilterSpy = jest.spyOn(filterServiceStub, 'clearFilters'); + const refreshFilterSpy = jest.spyOn(filterServiceStub, 'refreshTreeDataFilters'); const setItemsSpy = jest.spyOn(mockDataView, 'setItems'); const processSpy = jest.spyOn(sortServiceStub, 'processTreeDataInitialSort'); customElement.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file' } } as unknown as GridOption; customElement.initialization(slickEventHandler); + customElement.datasetHierarchical = mockHierarchical; customElement.datasetHierarchicalChanged(mockHierarchical); expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical); expect(clearFilterSpy).toHaveBeenCalled(); expect(processSpy).toHaveBeenCalled(); expect(setItemsSpy).toHaveBeenCalledWith([], 'id'); + setTimeout(() => { + expect(refreshFilterSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('should preset hierarchical dataset before the initialization and expect sortHierarchicalDataset to be called', () => { + const mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', parentId: 0 }]; + const mockHierarchical = [{ id: 0, file: 'documents', files: [{ id: 1, file: 'vacation.txt' }] }]; + const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set'); + const clearFilterSpy = jest.spyOn(filterServiceStub, 'clearFilters'); + const setItemsSpy = jest.spyOn(mockDataView, 'setItems'); + const processSpy = jest.spyOn(sortServiceStub, 'processTreeDataInitialSort'); + const sortHierarchicalSpy = jest.spyOn(treeDataServiceStub, 'sortHierarchicalDataset').mockReturnValue({ hierarchical: mockHierarchical as any[], flat: mockFlatDataset as any[] }); + + customElement.dispose(); + customElement.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file', initialSort: { columndId: 'file', direction: 'ASC' } } } as unknown as GridOption; + customElement.datasetHierarchical = mockHierarchical; + customElement.datasetHierarchicalChanged(mockHierarchical); + customElement.initialization(slickEventHandler); + + expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical); + expect(clearFilterSpy).toHaveBeenCalled(); + expect(processSpy).not.toHaveBeenCalled(); + expect(setItemsSpy).toHaveBeenCalledWith(mockFlatDataset, 'id'); + expect(sortHierarchicalSpy).toHaveBeenCalledWith(mockHierarchical); + }); + + it('should expect "refreshTreeDataFilters" method to be called when our flat dataset was already set and it just got changed a 2nd time', () => { + const mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', parentId: 0 }]; + const mockHierarchical = [{ id: 0, file: 'documents', files: [{ id: 1, file: 'vacation.txt' }] }]; + const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set'); + jest.spyOn(treeDataServiceStub, 'convertFlatParentChildToTreeDatasetAndSort').mockReturnValue({ hierarchical: mockHierarchical as any[], flat: mockFlatDataset as any[] }); + const refreshTreeSpy = jest.spyOn(filterServiceStub, 'refreshTreeDataFilters'); + + customElement.dataset = [{ id: 0, file: 'documents' }]; + customElement.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file', parentPropName: 'parentId', childrenPropName: 'files', initialSort: { columndId: 'file', direction: 'ASC' } } } as unknown as GridOption; + customElement.initialization(slickEventHandler); + customElement.dataset = mockFlatDataset; + customElement.datasetChanged(mockFlatDataset, []); + + expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical); + expect(refreshTreeSpy).toHaveBeenCalled(); }); }); }); diff --git a/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts b/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts index 738fb06df..02ebf5773 100644 --- a/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts +++ b/src/aurelia-slickgrid/custom-elements/aurelia-slickgrid.ts @@ -411,9 +411,8 @@ export class AureliaSlickgridCustomElement { this.grid.init(); if (!this.customDataView && this.dataview) { - this.dataview.beginUpdate(); - this.dataview.setItems(this._dataset, this.gridOptions.datasetIdPropertyName); - this.dataview.endUpdate(); + const initialDataset = this.gridOptions?.enableTreeData ? this.sortTreeDataset(this.dataset) : this.dataset; + this.dataview.setItems(initialDataset, this.gridOptions.datasetIdPropertyName); // if you don't want the items that are not visible (due to being filtered out or being on a different page) // to stay selected, pass 'false' to the second arg @@ -639,7 +638,7 @@ export class AureliaSlickgridCustomElement { datasetChanged(newDataset: any[], oldValue: any[]) { const prevDatasetLn = this._currentDatasetLength; - let data = [...newDataset]; + let data = newDataset; // when Tree Data is enabled and we don't yet have the hierarchical dataset filled, we can force a convert & sort of the array if (this.grid && this.gridOptions?.enableTreeData && Array.isArray(newDataset) && (newDataset.length > 0 || newDataset.length !== prevDatasetLn)) { @@ -647,6 +646,7 @@ export class AureliaSlickgridCustomElement { data = this.sortTreeDataset(newDataset); } + this._dataset = data; this.refreshGridData(data || []); this._currentDatasetLength = newDataset.length; diff --git a/src/aurelia-slickgrid/extensions/__tests__/rowDetailViewExtension.spec.ts b/src/aurelia-slickgrid/extensions/__tests__/rowDetailViewExtension.spec.ts index b648f50ba..4a3eda1f9 100644 --- a/src/aurelia-slickgrid/extensions/__tests__/rowDetailViewExtension.spec.ts +++ b/src/aurelia-slickgrid/extensions/__tests__/rowDetailViewExtension.spec.ts @@ -46,15 +46,14 @@ const mockSelectionModel = jest.fn().mockImplementation(() => ({ destroy: jest.fn() })); -jest.mock('slickgrid/plugins/slick.rowdetailview', () => mockAddon); -Slick.Plugins = { - RowDetailView: mockAddon -} as any; - -jest.mock('slickgrid/plugins/slick.rowselectionmodel', () => mockSelectionModel); -Slick.RowSelectionModel = mockSelectionModel; - describe('rowDetailViewExtension', () => { + jest.mock('slickgrid/plugins/slick.rowdetailview', () => mockAddon); + Slick.Plugins = { + RowDetailView: mockAddon + } as any; + jest.mock('slickgrid/plugins/slick.rowselectionmodel', () => mockSelectionModel); + + Slick.RowSelectionModel = mockSelectionModel; let pluginEa: EventAggregator; let pubSubService: PubSubService; let extension: RowDetailViewExtension; diff --git a/src/aurelia-slickgrid/global-grid-options.ts b/src/aurelia-slickgrid/global-grid-options.ts index ac3672b98..9430a82bd 100644 --- a/src/aurelia-slickgrid/global-grid-options.ts +++ b/src/aurelia-slickgrid/global-grid-options.ts @@ -1,4 +1,4 @@ -import { Column, DelimiterType, EventNamingStyle, FileType, Filters, OperatorType } from '@slickgrid-universal/common'; +import { Column, DelimiterType, EventNamingStyle, FileType, Filters, GridAutosizeColsMode, OperatorType, TreeDataOption } from '@slickgrid-universal/common'; import { GridOption, RowDetailView } from './models/index'; /** @@ -19,7 +19,8 @@ export const GlobalGridOptions: Partial = { }, cellHighlightCssClass: 'slick-cell-modified', checkboxSelector: { - cssClass: 'slick-cell-checkboxsel' + cssClass: 'slick-cell-checkboxsel', + width: 40 }, cellMenu: { autoAdjustDrop: true, @@ -28,6 +29,7 @@ export const GlobalGridOptions: Partial = { hideCommandSection: false, hideOptionSection: false, }, + columnGroupSeparator: ' - ', columnPicker: { fadeSpeed: 0, hideForceFitButton: false, @@ -38,6 +40,8 @@ export const GlobalGridOptions: Partial = { labels: { cancelButtonKey: 'CANCEL', cloneButtonKey: 'CLONE', + resetEditorButtonTooltipKey: 'RESET_INPUT_VALUE', + resetFormButtonKey: 'RESET_FORM', massSelectionButtonKey: 'APPLY_TO_SELECTION', massSelectionStatusKey: 'X_OF_Y_MASS_SELECTED', massUpdateButtonKey: 'APPLY_MASS_UPDATE', @@ -61,6 +65,9 @@ export const GlobalGridOptions: Partial = { hideExportTextDelimitedCommand: true, hideMenuOnScroll: true, hideOptionSection: false, + iconCollapseAllGroupsCommand: 'fa fa-compress', + iconExpandAllGroupsCommand: 'fa fa-expand', + iconClearGroupingCommand: 'fa fa-times', iconCopyCellValueCommand: 'fa fa-clone', iconExportCsvCommand: 'fa fa-download', iconExportExcelCommand: 'fa fa-file-excel-o text-success', @@ -79,10 +86,10 @@ export const GlobalGridOptions: Partial = { metricTexts: { items: 'items', itemsKey: 'ITEMS', - itemsSelected: 'items selected', - itemsSelectedKey: 'ITEMS_SELECTED', of: 'of', ofKey: 'OF', + itemsSelected: 'items selected', + itemsSelectedKey: 'ITEMS_SELECTED' } }, dataView: { @@ -95,12 +102,12 @@ export const GlobalGridOptions: Partial = { defaultSlickgridEventPrefix: '', defaultFilter: Filters.input, defaultFilterPlaceholder: '🔎︎', // magnifying glass icon - defaultFilterRangeOperator: OperatorType.rangeExclusive, defaultBackendServiceFilterTypingDebounce: 500, + enableFilterTrimWhiteSpace: false, // do we want to trim white spaces on all Filters? + defaultFilterRangeOperator: OperatorType.rangeInclusive, editable: false, - enableAutoResize: true, - enableAutoSizeColumns: true, - enableHeaderMenu: true, + editorTypingDebounce: 450, + filterTypingDebounce: 0, enableEmptyDataWarningMessage: true, emptyDataWarning: { className: 'slick-empty-data-warning', @@ -113,15 +120,18 @@ export const GlobalGridOptions: Partial = { frozenLeftViewportMarginLeft: '0px', frozenRightViewportMarginLeft: '40%', }, + enableAutoResize: true, + enableAutoSizeColumns: true, enableCellNavigation: false, - enableCheckboxSelector: false, enableColumnPicker: true, enableColumnReorder: true, + enableColumnResizeOnDoubleClick: true, enableContextMenu: true, - enableExcelExport: false, // both exports are now opt-in + enableExcelExport: false, enableTextExport: false, - enableFilterTrimWhiteSpace: false, // do we want to trim white spaces on all Filters? enableGridMenu: true, + enableHeaderMenu: true, + enableMouseHoverHighlightRow: true, enableSorting: true, enableTextSelectionOnCells: true, eventNamingStyle: EventNamingStyle.kebabCase, @@ -132,8 +142,8 @@ export const GlobalGridOptions: Partial = { filename: 'export', format: FileType.xlsx, groupingColumnHeaderTitle: 'Group By', - groupCollapsedSymbol: '\u25B9', - groupExpandedSymbol: '\u25BF', + groupCollapsedSymbol: '⮞', + groupExpandedSymbol: '⮟', groupingAggregatorRowText: '', sanitizeDataExport: false, }, @@ -147,10 +157,21 @@ export const GlobalGridOptions: Partial = { sanitizeDataExport: false, useUtf8WithBom: true }, - filterTypingDebounce: 0, + gridAutosizeColsMode: GridAutosizeColsMode.none, forceFitColumns: false, frozenHeaderWidthCalcDifferential: 1, gridMenu: { + commandLabels: { + clearAllFiltersCommandKey: 'CLEAR_ALL_FILTERS', + clearAllSortingCommandKey: 'CLEAR_ALL_SORTING', + clearFrozenColumnsCommandKey: 'CLEAR_PINNING', + exportCsvCommandKey: 'EXPORT_TO_CSV', + exportExcelCommandKey: 'EXPORT_TO_EXCEL', + exportTextDelimitedCommandKey: 'EXPORT_TO_TAB_DELIMITED', + refreshDatasetCommandKey: 'REFRESH_DATASET', + toggleFilterCommandKey: 'TOGGLE_FILTER_ROW', + togglePreHeaderCommandKey: 'TOGGLE_PRE_HEADER_ROW', + }, hideClearAllFiltersCommand: false, hideClearAllSortingCommand: false, hideClearFrozenColumnsCommand: true, // opt-in command @@ -159,15 +180,15 @@ export const GlobalGridOptions: Partial = { hideExportTextDelimitedCommand: true, hideForceFitButton: false, hideRefreshDatasetCommand: false, + hideSyncResizeButton: true, hideToggleFilterCommand: false, hideTogglePreHeaderCommand: false, - hideSyncResizeButton: true, iconCssClass: 'fa fa-bars', - iconClearAllFiltersCommand: 'fa fa-filter text-danger', - iconClearAllSortingCommand: 'fa fa-unsorted text-danger', + iconClearAllFiltersCommand: 'fa fa-filter', + iconClearAllSortingCommand: 'fa fa-unsorted', iconClearFrozenColumnsCommand: 'fa fa-times', iconExportCsvCommand: 'fa fa-download', - iconExportExcelCommand: 'fa fa-file-excel-o text-success', + iconExportExcelCommand: 'fa fa-file-excel-o', iconExportTextDelimitedCommand: 'fa fa-download', iconRefreshDatasetCommand: 'fa fa-refresh', iconToggleFilterCommand: 'fa fa-random', @@ -181,23 +202,25 @@ export const GlobalGridOptions: Partial = { autoAlign: true, autoAlignOffset: 12, minWidth: 140, - iconClearFilterCommand: 'fa fa-filter text-danger', + iconClearFilterCommand: 'fa fa-filter', iconClearSortCommand: 'fa fa-unsorted', iconFreezeColumns: 'fa fa-thumb-tack', iconSortAscCommand: 'fa fa-sort-amount-asc', iconSortDescCommand: 'fa fa-sort-amount-desc', iconColumnHideCommand: 'fa fa-times', + iconColumnResizeByContentCommand: 'fa fa-arrows-h', + hideColumnResizeByContentCommand: false, hideColumnHideCommand: false, hideClearFilterCommand: false, hideClearSortCommand: false, hideFreezeColumnsCommand: true, // opt-in command - hideSortCommands: false, - hideSortCommandsDivider: false + hideSortCommands: false }, - headerRowHeight: 35, multiColumnSort: true, numberedMultiColumnSort: true, tristateMultiColumnSort: false, + sortColNumberInSeparateSpan: true, + suppressActiveCellChangeOnEdit: true, pagination: { pageSizes: [10, 15, 20, 25, 30, 40, 50, 75, 100], pageSize: 25, @@ -212,11 +235,25 @@ export const GlobalGridOptions: Partial = { saveDetailViewOnScroll: false, viewModel: '', } as RowDetailView, + headerRowHeight: 35, rowHeight: 35, - sortColNumberInSeparateSpan: true, - suppressActiveCellChangeOnEdit: true, - topPanelHeight: 35, + topPanelHeight: 30, translationNamespaceSeparator: ':', + resizeByContentOnlyOnFirstLoad: true, + resizeByContentOptions: { + alwaysRecalculateColumnWidth: false, + cellCharWidthInPx: 7.8, + cellPaddingWidthInPx: 14, + defaultRatioForStringType: 0.88, + formatterPaddingWidthInPx: 0, + maxItemToInspectCellContentWidth: 1000, + maxItemToInspectSingleColumnWidthByContent: 5000, + widthToRemoveFromExceededWidthReadjustment: 50, + }, + treeDataOptions: { + exportIndentMarginLeft: 5, + exportIndentationLeadingChar: '͏͏͏͏͏͏͏͏͏·', + } as unknown as TreeDataOption }; /** @@ -224,10 +261,11 @@ export const GlobalGridOptions: Partial = { * when using Column Header Grouping, we'll prefix the column group title * else we'll simply return the column name title */ -function pickerHeaderColumnValueExtractor(column: Column) { - const headerGroup = column && column.columnGroup || ''; +function pickerHeaderColumnValueExtractor(column: Column, gridOptions?: GridOption) { + const headerGroup = column?.columnGroup || ''; + const columnGroupSeparator = gridOptions?.columnGroupSeparator ?? ' - '; if (headerGroup) { - return `${headerGroup} - ${column.name}`; + return headerGroup + columnGroupSeparator + column.name; } return column?.name ?? ''; } diff --git a/src/aurelia-slickgrid/services/__tests__/resizer.service.spec.ts b/src/aurelia-slickgrid/services/__tests__/resizer.service.spec.ts index 24748abea..197e9b901 100644 --- a/src/aurelia-slickgrid/services/__tests__/resizer.service.spec.ts +++ b/src/aurelia-slickgrid/services/__tests__/resizer.service.spec.ts @@ -1,6 +1,5 @@ import { Column, Editors, FieldType, GridOption, SlickGrid, SlickNamespace, } from '@slickgrid-universal/common'; import { EventAggregator } from 'aurelia-event-aggregator'; - import { PubSubService } from '../pubSub.service'; import { ResizerService } from '../resizer.service'; @@ -40,6 +39,7 @@ const gridStub = { getColumnIndex: jest.fn(), getColumns: jest.fn(), getOptions: jest.fn(), + getViewports: jest.fn(), getData: () => mockDataView, getUID: () => GRID_UID, reRenderColumns: jest.fn(), @@ -50,6 +50,7 @@ const gridStub = { setPreHeaderPanelVisibility: jest.fn(), setOptions: jest.fn(), setSortColumns: jest.fn(), + onColumnsResizeDblClick: new Slick.Event(), onSort: new Slick.Event(), } as unknown as SlickGrid; @@ -86,6 +87,7 @@ describe('Resizer Service', () => { createPreHeaderPanel: true, showPreHeaderPanel: true, preHeaderPanelHeight: 20, + resizeByContentOptions: {}, } as GridOption; jest.spyOn(gridStub, 'getOptions').mockReturnValue(mockGridOptions); }); @@ -254,12 +256,12 @@ describe('Resizer Service', () => { let mockData: any[]; beforeEach(() => { - mockGridOptions.resizeCellCharWidthInPx = 7; - mockGridOptions.resizeCellPaddingWidthInPx = 6; - mockGridOptions.resizeFormatterPaddingWidthInPx = 5; - mockGridOptions.resizeDefaultRatioForStringType = 0.88; - mockGridOptions.resizeAlwaysRecalculateColumnWidth = false; - mockGridOptions.resizeMaxItemToInspectCellContentWidth = 4; + mockGridOptions.resizeByContentOptions.cellCharWidthInPx = 7; + mockGridOptions.resizeByContentOptions.cellPaddingWidthInPx = 6; + mockGridOptions.resizeByContentOptions.formatterPaddingWidthInPx = 5; + mockGridOptions.resizeByContentOptions.defaultRatioForStringType = 0.88; + mockGridOptions.resizeByContentOptions.alwaysRecalculateColumnWidth = false; + mockGridOptions.resizeByContentOptions.maxItemToInspectCellContentWidth = 4; mockColDefs = [ // typically the `originalWidth` is set by the columnDefinitiosn setter in vanilla grid bundle but we can mock it for our test { id: 'userId', field: 'userId', width: 30, originalWidth: 30 }, @@ -269,23 +271,69 @@ describe('Resizer Service', () => { { id: 'age', field: 'age', type: FieldType.number, resizeExtraWidthPadding: 2 }, { id: 'street', field: 'street', maxWidth: 15 }, { id: 'country', field: 'country', maxWidth: 15, resizeMaxWidthThreshold: 14, rerenderOnResize: true }, + { id: 'zip', field: 'zip', width: 20, type: 'number' }, ] as Column[]; mockData = [ - { userId: 1, firstName: 'John', lastName: 'Doe', gender: 'male', age: 20, street: '478 Kunze Land', country: 'United States of America' }, - { userId: 2, firstName: 'Destinee', lastName: 'Shanahan', gender: 'female', age: 25, street: '20519 Watson Lodge', country: 'Australia' }, - { userId: 3, firstName: 'Sarai', lastName: 'Altenwerth', gender: 'female', age: 30, street: '184 Preston Pine', country: 'United States of America' }, - { userId: 4, firstName: 'Tyshawn', lastName: 'Hyatt', gender: 'male', age: 35, street: '541 Senger Drives', country: 'Canada' }, - { userId: 5, firstName: 'Alvina', lastName: 'Franecki', gender: 'female', age: 100, street: '20229 Tia Turnpike', country: 'United States of America' }, - { userId: 6, firstName: 'Therese', lastName: 'Brakus', gender: 'female', age: 99, street: '34767 Lindgren Dam', country: 'Bosnia' }, + { userId: 1, firstName: 'John', lastName: 'Doe', gender: 'male', age: 20, street: '478 Kunze Land', country: 'United States of America', zip: 123456 }, + { userId: 2, firstName: 'Destinee', lastName: 'Shanahan', gender: 'female', age: 25, street: '20519 Watson Lodge', country: 'Australia', zip: 223344 }, + { userId: 3, firstName: 'Sarai', lastName: 'Altenwerth', gender: 'female', age: 30, street: '184 Preston Pine', country: 'United States of America', zip: 334433 }, + { userId: 4, firstName: 'Tyshawn', lastName: 'Hyatt', gender: 'male', age: 35, street: '541 Senger Drives', country: 'Canada', zip: 444455 }, + { userId: 5, firstName: 'Alvina', lastName: 'Franecki', gender: 'female', age: 100, street: '20229 Tia Turnpike', country: 'United States of America', zip: 777555 }, + { userId: 6, firstName: 'Therese', lastName: 'Brakus', gender: 'female', age: 99, street: '34767 Lindgren Dam', country: 'Bosnia', zip: 654321 }, ]; jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColDefs); jest.spyOn(mockDataView, 'getItems').mockReturnValue(mockData); }); + it('should call handleSingleColumnResizeByContent when "onHeaderMenuColumnResizeByContent" gets triggered', () => { + const reRenderSpy = jest.spyOn(gridStub, 'reRenderColumns'); + + mockGridOptions.enableColumnResizeOnDoubleClick = true; + service.init(gridStub, divContainer); + pubSubService.publish('onHeaderMenuColumnResizeByContent', { columnId: 'firstName' }); + + expect(reRenderSpy).toHaveBeenCalledWith(false); + expect(mockColDefs[1].width).toBe(56); // longest word "Destinee" (length 8 * charWidth(7) * ratio(0.88)) + cellPadding(6) = 55.28 ceil to => 56 + }); + + it('should call handleSingleColumnResizeByContent when "onHeaderMenuColumnResizeByContent" gets triggered', () => { + const reRenderSpy = jest.spyOn(gridStub, 'reRenderColumns'); + + mockGridOptions.enableColumnResizeOnDoubleClick = true; + service.init(gridStub, divContainer); + gridStub.onColumnsResizeDblClick.notify({ triggeredByColumn: 'zip', grid: gridStub }); + + expect(reRenderSpy).toHaveBeenCalledWith(false); + expect(mockColDefs[7].width).toBe(48); // longest number "777555" (length 6 * charWidth(7)) + cellPadding(6) = 48 + }); + + it('should call handleSingleColumnResizeByContent when "onHeaderMenuColumnResizeByContent" gets triggered but expect a resized column width when left section width becomes greater than full viewport width', () => { + const viewportLeft = document.createElement('div'); + viewportLeft.className = 'slick-viewport-left'; + Object.defineProperty(viewportLeft, 'clientWidth', { writable: true, configurable: true, value: 250 }); + + const viewportRight = document.createElement('div'); + viewportRight.className = 'slick-viewport-right'; + Object.defineProperty(viewportRight, 'clientWidth', { writable: true, configurable: true, value: 27 }); + + jest.spyOn(gridStub, 'getViewports').mockReturnValue([viewportLeft, viewportRight]); + const reRenderSpy = jest.spyOn(gridStub, 'reRenderColumns'); + + mockGridOptions.frozenColumn = 7; + mockGridOptions.enableColumnResizeOnDoubleClick = true; + mockGridOptions.resizeByContentOptions.widthToRemoveFromExceededWidthReadjustment = 20; + service.init(gridStub, divContainer); + gridStub.onColumnsResizeDblClick.notify({ triggeredByColumn: 'zip', grid: gridStub }); + + expect(reRenderSpy).toHaveBeenCalledWith(false); + expect(mockColDefs[7].width).toBeLessThan(30); + }); + it('should call the resize and expect first column have a fixed width while other will have a calculated width when resizing by their content', () => { const setColumnsSpy = jest.spyOn(gridStub, 'setColumns'); const reRenderColumnsSpy = jest.spyOn(gridStub, 'reRenderColumns'); + const pubSubSpy = jest.spyOn(pubSubService, 'publish'); service.init(gridStub, divContainer); service.resizeColumnsByCellContent(true); @@ -297,15 +345,22 @@ describe('Resizer Service', () => { expect.objectContaining({ id: 'lastName', width: 68 }), // longest word "Altenwerth" (length 10 * charWidth(7) * ratio(0.88)) + cellPadding(6) = 67.6 ceil to => 68 expect.objectContaining({ id: 'gender', width: 57 }), // longest word "female" (length 6 * charWidth(7) * customRatio(1.2)) + cellPadding(6) = 56.4 ceil to 57 expect.objectContaining({ id: 'age', width: 29 }), // longest number 100 (length 3 * charWidth(7) * ratio(1)) + cellPadding(6) + extraPadding(2) = 44.96 ceil to 45 - expect.objectContaining({ id: 'street', width: 15 }), // longest number "20229 Tia Turnpike" goes over maxWidth so we fallback to it - expect.objectContaining({ id: 'country', width: 14 }), // longest number "United States of America" goes over resizeMaxWidthThreshold so we fallback to it + expect.objectContaining({ id: 'street', width: 15 }), // longest text "20229 Tia Turnpike" goes over maxWidth so we fallback to it + expect.objectContaining({ id: 'country', width: 14 }), // longest text "United States of America" goes over resizeMaxWidthThreshold so we fallback to it + expect.objectContaining({ id: 'zip', width: 48 }), // longest number "777555" ])); expect(reRenderColumnsSpy).toHaveBeenCalledWith(true); + expect(pubSubSpy).toBeCalledWith('onBeforeResizeByContent'); + expect(pubSubSpy).toBeCalledWith('onAfterResizeByContent', { + calculateColumnWidths: { userId: 30, firstName: 56, lastName: 68, gender: 57, age: 29, street: 15, country: 14, zip: 48 }, + readItemCount: 5 + }); }); it('should call the resize and expect first column have a fixed width while other will have a calculated width when resizing by their content and grid is editable', () => { const setColumnsSpy = jest.spyOn(gridStub, 'setColumns'); const reRenderColumnsSpy = jest.spyOn(gridStub, 'reRenderColumns'); + const pubSubSpy = jest.spyOn(pubSubService, 'publish'); mockGridOptions.editable = true; service.init(gridStub, divContainer); @@ -319,10 +374,25 @@ describe('Resizer Service', () => { expect.objectContaining({ id: 'lastName', width: 73 }), // longest word "Altenwerth" (length 10 * charWidth(7) * ratio(0.88)) + cellPadding(6) + editorPadding(5) = 72.6 ceil to => 73 expect.objectContaining({ id: 'gender', width: 57 }), // longest word "female" (length 6 * charWidth(7) * customRatio(1.2)) + cellPadding(6) = 56.4 ceil to 57 expect.objectContaining({ id: 'age', width: 29 }), // longest number 100 (length 3 * charWidth(7) * ratio(1)) + cellPadding(6) + extraPadding(2) = 44.96 ceil to 45 - expect.objectContaining({ id: 'street', width: 15 }), // longest number "20229 Tia Turnpike" goes over maxWidth so we fallback to it - expect.objectContaining({ id: 'country', width: 14 }), // longest number "United States of America" goes over resizeMaxWidthThreshold so we fallback to it + expect.objectContaining({ id: 'street', width: 15 }), // longest text "20229 Tia Turnpike" goes over maxWidth so we fallback to it + expect.objectContaining({ id: 'country', width: 14 }), // longest text "United States of America" goes over resizeMaxWidthThreshold so we fallback to it + expect.objectContaining({ id: 'zip', width: 48 }), // longest number "777555" ])); expect(reRenderColumnsSpy).toHaveBeenCalledWith(true); + expect(pubSubSpy).toBeCalledWith('onBeforeResizeByContent'); + expect(pubSubSpy).toBeCalledWith('onAfterResizeByContent', { + calculateColumnWidths: { userId: 30, firstName: 61, lastName: 73, gender: 57, age: 29, street: 15, country: 14, zip: 48 }, + readItemCount: 5 + }); + }); + + it('should call "resizeColumnsByCellContent" when "onFullResizeByContentRequested" pubsub event is triggered', () => { + const resizeSpy = jest.spyOn(service, 'resizeColumnsByCellContent'); + + service.init(gridStub, divContainer); + pubSubService.publish('onFullResizeByContentRequested', { caller: 'GridStateService' }); + + expect(resizeSpy).toHaveBeenCalledWith(true); }); }); diff --git a/src/aurelia-slickgrid/services/resizer.service.ts b/src/aurelia-slickgrid/services/resizer.service.ts index d532091f8..6b3755294 100644 --- a/src/aurelia-slickgrid/services/resizer.service.ts +++ b/src/aurelia-slickgrid/services/resizer.service.ts @@ -1,8 +1,10 @@ import { + Column, FieldType, GetSlickEventType, GridSize, parseFormatterWhenExist, + ResizeByContentOption, sanitizeHtmlToText, SlickDataView, SlickEventData, @@ -27,6 +29,7 @@ export class ResizerService { private _addon!: SlickResizer; private _eventHandler: SlickEventHandler; private _gridParentContainerElm?: HTMLElement; + private _hasResizedByContentAtLeastOnce = false; private _lastDimensions?: GridSize; private _totalColumnsWidthByContent = 0; @@ -44,6 +47,10 @@ export class ResizerService { return this._grid?.getData() as SlickDataView; } + get resizeByContentOptions(): ResizeByContentOption { + return this.gridOptions?.resizeByContentOptions ?? {}; + } + constructor(private readonly pubSubService: PubSubService) { this._eventHandler = new Slick.EventHandler(); } @@ -108,6 +115,22 @@ export class ResizerService { // resize by content could be called from the outside by other services via pub/sub event this.pubSubService.subscribe('onFullResizeByContentRequested', () => this.resizeColumnsByCellContent(true)); } + + // resize by content could be called from the outside by other services via pub/sub event + this.pubSubService.subscribe('onFullResizeByContentRequested', () => this.resizeColumnsByCellContent(true)); + } + + // on double-click resize, should we resize the cell by its cell content? + // the same action can be called from a double-click and/or from column header menu + if (this.gridOptions?.enableColumnResizeOnDoubleClick) { + this.pubSubService.subscribe('onHeaderMenuColumnResizeByContent', (data => { + this.handleSingleColumnResizeByContent(data.columnId); + })); + + const onColumnsResizeDblClickHandler = this._grid.onColumnsResizeDblClick; + (this._eventHandler as SlickEventHandler>).subscribe(onColumnsResizeDblClickHandler, (_e, args) => { + this.handleSingleColumnResizeByContent(args.triggeredByColumn); + }); } } } @@ -140,28 +163,25 @@ export class ResizerService { } /** - * Resize each column width by their cell text/value content (this could potentially go wider than the viewport and end up showing an horizontal scroll). - * This operation requires to loop through each dataset item to inspect each cell content width and has a performance cost associated to this process. - * - * NOTE: please that for performance reasons we will only inspect the first 1000 rows, - * however user could override it by using the grid option `resizeMaxItemToInspectCellContentWidth` to increase/decrease how many items to inspect. - * @param {Boolean} recalculateColumnsTotalWidth - defaults to false, do we want to recalculate the necessary total columns width even if it was already calculated? - */ + * Resize each column width by their cell text/value content (this could potentially go wider than the viewport and end up showing an horizontal scroll). + * This operation requires to loop through each dataset item to inspect each cell content width and has a performance cost associated to this process. + * + * NOTE: please that for performance reasons we will only inspect the first 1000 rows, + * however user could override it by using the grid option `resizeMaxItemToInspectCellContentWidth` to increase/decrease how many items to inspect. + * @param {Boolean} recalculateColumnsTotalWidth - defaults to false, do we want to recalculate the necessary total columns width even if it was already calculated? + */ resizeColumnsByCellContent(recalculateColumnsTotalWidth = false) { const columnDefinitions = this._grid.getColumns(); const dataset = this.dataView.getItems() as any[]; const columnWidths: { [columnId in string | number]: number; } = {}; let reRender = false; + let readItemCount = 0; - if (!Array.isArray(dataset) || dataset.length === 0) { + if ((!Array.isArray(dataset) || dataset.length === 0) || (this._hasResizedByContentAtLeastOnce && this.gridOptions?.resizeByContentOnlyOnFirstLoad && !recalculateColumnsTotalWidth)) { return; } - // read a few optional resize by content grid options - const resizeCellCharWidthInPx = this.gridOptions.resizeCellCharWidthInPx ?? 7; // width in pixels of a string character, this can vary depending on which font family/size is used & cell padding - const resizeCellPaddingWidthInPx = this.gridOptions.resizeCellPaddingWidthInPx ?? 6; - const resizeFormatterPaddingWidthInPx = this.gridOptions.resizeFormatterPaddingWidthInPx ?? 6; - const resizeMaxItemToInspectCellContentWidth = this.gridOptions.resizeMaxItemToInspectCellContentWidth ?? 1000; // how many items do we want to analyze cell content with widest width + this.pubSubService.publish('onBeforeResizeByContent'); // calculate total width necessary by each cell content // we won't re-evaluate if we already had calculated the total @@ -171,34 +191,13 @@ export class ResizerService { columnWidths[columnDef.id] = columnDef.originalWidth ?? columnDef.minWidth ?? 0; } - // loop through the entire dataset (limit to first 1000 rows), and evaluate the width by its content - // if we have a Formatter, we will also potentially add padding - dataset.every((item: any, rowIdx: number) => { - if (rowIdx > resizeMaxItemToInspectCellContentWidth) { - return false; - } - columnDefinitions.forEach((columnDef, colIdx) => { - if (!columnDef.originalWidth) { - const charWidthPx = columnDef?.resizeCharWidthInPx ?? resizeCellCharWidthInPx; - const formattedData = parseFormatterWhenExist(columnDef?.formatter, rowIdx, colIdx, columnDef, item, this._grid); - const formattedDataSanitized = sanitizeHtmlToText(formattedData); - const formattedTextWidthInPx = Math.ceil(formattedDataSanitized.length * charWidthPx); - const resizeMaxWidthThreshold = columnDef.resizeMaxWidthThreshold; - if (columnDef && (columnWidths[columnDef.id] === undefined || formattedTextWidthInPx > columnWidths[columnDef.id])) { - columnWidths[columnDef.id] = (resizeMaxWidthThreshold !== undefined && formattedTextWidthInPx > resizeMaxWidthThreshold) - ? resizeMaxWidthThreshold - : (columnDef.maxWidth !== undefined && formattedTextWidthInPx > columnDef.maxWidth) ? columnDef.maxWidth : formattedTextWidthInPx; - } - } - }); - return true; - }); + // calculate cell width by reading all data from dataset and also parse through any Formatter(s) when exist + readItemCount = this.calculateCellWidthByReadingDataset(columnDefinitions, columnWidths, this.resizeByContentOptions.maxItemToInspectCellContentWidth); // finally loop through all column definitions one last time to apply new calculated `width` on each elligible column let totalColsWidth = 0; for (const column of columnDefinitions) { - const fieldType = column?.filter?.type ?? column?.type ?? FieldType.string; - const resizeAlwaysRecalculateWidth = column.resizeAlwaysRecalculateWidth ?? this.gridOptions?.resizeAlwaysRecalculateColumnWidth ?? false; + const resizeAlwaysRecalculateWidth = column.resizeAlwaysRecalculateWidth ?? this.resizeByContentOptions.alwaysRecalculateColumnWidth ?? false; if (column.originalWidth && !resizeAlwaysRecalculateWidth) { column.width = column.originalWidth; @@ -208,37 +207,7 @@ export class ResizerService { } // let's start with column width found in previous column & data analysis - let newColWidth = columnWidths[column.id]; - - // apply optional ratio which is typically 1, except for string where we use a ratio of around ~0.9 since we have more various thinner characters like (i, l, t, ...) - const stringWidthRatio = column?.resizeCalcWidthRatio ?? this.gridOptions?.resizeDefaultRatioForStringType ?? 0.9; - newColWidth *= fieldType === 'string' ? stringWidthRatio : 1; - - // apply extra cell padding, custom padding & editor formatter padding - // -- - newColWidth += resizeCellPaddingWidthInPx; - if (column.resizeExtraWidthPadding) { - newColWidth += column.resizeExtraWidthPadding; - } - if (column.editor && this.gridOptions.editable) { - newColWidth += resizeFormatterPaddingWidthInPx; - } - - // make sure we're not over a column max width and/or optional custom max width threshold - if (column.maxWidth !== undefined && newColWidth > column.maxWidth) { - newColWidth = column.maxWidth; - } - if (column.resizeMaxWidthThreshold !== undefined && newColWidth > column.resizeMaxWidthThreshold) { - newColWidth = column.resizeMaxWidthThreshold; - } - - // make the value the closest bottom integer - newColWidth = Math.ceil(newColWidth); - - // finally only apply the new width if user didn't yet provide one and/or if user really wants to specifically ask for a recalculate - if (column.originalWidth === undefined || column.resizeAlwaysRecalculateWidth === true || this.gridOptions?.resizeAlwaysRecalculateColumnWidth === true) { - column.width = Math.ceil(newColWidth); - } + this.applyNewCalculatedColumnWidthByReference(column, columnWidths[column.id]); } // add the new column width to the total width which we'll use later to compare against viewport width @@ -249,10 +218,177 @@ export class ResizerService { // send updated column definitions widths to SlickGrid this._grid.setColumns(columnDefinitions); + this._hasResizedByContentAtLeastOnce = true; + + const calculateColumnWidths: { [columnId in string | number]: number | undefined; } = {}; + for (const columnDef of columnDefinitions) { + calculateColumnWidths[columnDef.id] = columnDef.width; + } // get the grid container viewport width and if our viewport calculated total columns is greater than the viewport width // then we'll call reRenderColumns() when getting wider than viewport or else the default autosizeColumns() when we know we have plenty of space to shrink the columns const viewportWidth = this._gridParentContainerElm?.offsetWidth ?? 0; this._totalColumnsWidthByContent > viewportWidth ? this._grid.reRenderColumns(reRender) : this._grid.autosizeColumns(); + this.pubSubService.publish('onAfterResizeByContent', { readItemCount, calculateColumnWidths }); + } + + + // -- + // private functions + // ------------------ + + /** + * Step 1 - The first step will read through the entire dataset (unless max item count is reached), + * it will analyze each cell of the grid and calculate its max width via its content and column definition info (it will do so by calling step 2 method while looping through each cell). + * @param columnOrColumns - single or array of column definition(s) + * @param columnWidths - column width object that will be updated by reference pointers + * @param columnIndexOverride - an optional column index, if provided it will override the column index position + * @returns - count of items that was read + */ + private calculateCellWidthByReadingDataset(columnOrColumns: Column | Column[], columnWidths: { [columnId in string | number]: number; }, maxItemToInspect = 1000, columnIndexOverride?: number) { + const columnDefinitions = Array.isArray(columnOrColumns) ? columnOrColumns : [columnOrColumns]; + + // const columnDefinitions = this._grid.getColumns(); + const dataset = this.dataView.getItems() as any[]; + + let readItemCount = 0; + for (const [rowIdx, item] of dataset.entries()) { + if (rowIdx > maxItemToInspect) { + break; + } + if (Array.isArray(columnDefinitions)) { + if (typeof columnWidths === 'object') { + columnDefinitions.forEach((columnDef, colIdx) => { + const newColumnWidth = this.calculateCellWidthByContent(item, columnDef, rowIdx, columnIndexOverride ?? colIdx, columnWidths[columnDef.id]); + if (newColumnWidth !== undefined) { + columnWidths[columnDef.id] = newColumnWidth; + } + }); + } + } + readItemCount = rowIdx + 1; + } + + return readItemCount; + } + + /** + * Step 2 - This step will parse any Formatter(s) if defined, it will then sanitize any HTML tags and calculate the max width from that cell content. + * This function will be executed on every cell of the grid data. + * @param {Object} item - item data context object + * @param {Object} columnDef - column definition + * @param {Number} rowIdx - row index + * @param {Number} colIdx - column (cell) index + * @param {Number} initialMininalColumnWidth - initial width, could be coming from `minWidth` or a default `width` + * @returns - column width + */ + private calculateCellWidthByContent(item: any, columnDef: Column, rowIdx: number, colIdx: number, initialMininalColumnWidth?: number): number | undefined { + const resizeCellCharWidthInPx = this.resizeByContentOptions.cellCharWidthInPx ?? 7; // width in pixels of a string character, this can vary depending on which font family/size is used & cell padding + + if (!columnDef.originalWidth) { + const charWidthPx = columnDef?.resizeCharWidthInPx ?? resizeCellCharWidthInPx; + const formattedData = parseFormatterWhenExist(columnDef?.formatter, rowIdx, colIdx, columnDef, item, this._grid); + const formattedDataSanitized = sanitizeHtmlToText(formattedData); + const formattedTextWidthInPx = Math.ceil(formattedDataSanitized.length * charWidthPx); + const resizeMaxWidthThreshold = columnDef.resizeMaxWidthThreshold; + if (columnDef && (initialMininalColumnWidth === undefined || formattedTextWidthInPx > initialMininalColumnWidth)) { + initialMininalColumnWidth = (resizeMaxWidthThreshold !== undefined && formattedTextWidthInPx > resizeMaxWidthThreshold) + ? resizeMaxWidthThreshold + : (columnDef.maxWidth !== undefined && formattedTextWidthInPx > columnDef.maxWidth) ? columnDef.maxWidth : formattedTextWidthInPx; + } + } + return initialMininalColumnWidth; + } + + /** + * Step 3 - Apply the new calculated width, it might or might not use this calculated width depending on a few conditions. + * One of those condition will be to check that the new width doesn't go over a maxWidth and/or a maxWidthThreshold + * @param {Object} column - column definition to apply the width + * @param {Number} calculatedColumnWidth - new calculated column width to possibly apply + */ + private applyNewCalculatedColumnWidthByReference(column: Column, calculatedColumnWidth: number) { + // read a few optional resize by content grid options + const resizeCellPaddingWidthInPx = this.resizeByContentOptions.cellPaddingWidthInPx ?? 6; + const resizeFormatterPaddingWidthInPx = this.resizeByContentOptions.formatterPaddingWidthInPx ?? 6; + const fieldType = column?.filter?.type ?? column?.type ?? FieldType.string; + + // let's start with column width found in previous column & data analysis + let newColWidth = calculatedColumnWidth; + + // apply optional ratio which is typically 1, except for string where we use a ratio of around ~0.9 since we have more various thinner characters like (i, l, t, ...) + const stringWidthRatio = column?.resizeCalcWidthRatio ?? this.resizeByContentOptions.defaultRatioForStringType ?? 0.9; + newColWidth *= fieldType === 'string' ? stringWidthRatio : 1; + + // apply extra cell padding, custom padding & editor formatter padding + // -- + newColWidth += resizeCellPaddingWidthInPx; + if (column.resizeExtraWidthPadding) { + newColWidth += column.resizeExtraWidthPadding; + } + if (column.editor && this.gridOptions.editable) { + newColWidth += resizeFormatterPaddingWidthInPx; + } + + // make sure we're not over a column max width and/or optional custom max width threshold + if (column.maxWidth !== undefined && newColWidth > column.maxWidth) { + newColWidth = column.maxWidth; + } + if (column.resizeMaxWidthThreshold !== undefined && newColWidth > column.resizeMaxWidthThreshold) { + newColWidth = column.resizeMaxWidthThreshold; + } + + // make the value the closest bottom integer + newColWidth = Math.ceil(newColWidth); + + // finally only apply the new width if user didn't yet provide one and/or if user really wants to specifically ask for a recalculate + if (column.originalWidth === undefined || column.resizeAlwaysRecalculateWidth === true || this.resizeByContentOptions.alwaysRecalculateColumnWidth === true) { + column.width = this.readjustNewColumnWidthWhenOverLimit(column, newColWidth); + } + } + + private handleSingleColumnResizeByContent(columnId: string) { + const columnDefinitions = this._grid.getColumns(); + const columnDefIdx = columnDefinitions.findIndex(col => col.id === columnId); + + if (columnDefIdx >= 0) { + // provide the initial column width by reference to the calculation and the result will also be returned by reference + const columnDef = columnDefinitions[columnDefIdx]; + const columnWidths = { [columnId]: columnDef.originalWidth ?? columnDef.minWidth ?? 0 }; + columnDef.originalWidth = undefined; // reset original width since we want to recalculate it + this.calculateCellWidthByReadingDataset(columnDef, columnWidths, this.resizeByContentOptions.maxItemToInspectSingleColumnWidthByContent, columnDefIdx); + this.applyNewCalculatedColumnWidthByReference(columnDef, columnWidths[columnId]); + + // finally call the re-render for the UI to render the new column width + this._grid.reRenderColumns(columnDef?.rerenderOnResize ?? false); + } + } + + /** + * Checks wether the new calculated column width is valid or not, if it's not then return a lower and acceptable width. + * When using frozen (pinned) column, we cannot make our column wider than the grid viewport because it would become unusable/unscrollable + * and so if we do reach that threshold then our calculated column width becomes officially invalid + * @param {Object} column - column definition + * @param {Number} newColumnWidth - calculated column width input + * @returns boolean + */ + private readjustNewColumnWidthWhenOverLimit(column: Column, newColumnWidth: number): number { + const frozenColumnIdx = this.gridOptions.frozenColumn ?? -1; + const columnIdx = this._grid.getColumns().findIndex(col => col.id === column.id) ?? 0; + let adjustedWidth = newColumnWidth; + + if (frozenColumnIdx >= 0 && columnIdx <= frozenColumnIdx) { + const allViewports = Array.from(this._grid.getViewports()); + const leftViewportWidth = allViewports.find(viewport => viewport.classList.contains('slick-viewport-left'))?.clientWidth ?? 0; + const rightViewportWidth = allViewports.find(viewport => viewport.classList.contains('slick-viewport-right'))?.clientWidth ?? 0; + const viewportFullWidth = leftViewportWidth + rightViewportWidth; + const leftViewportWidthMinusCurrentCol = leftViewportWidth - (column.width ?? 0); + const isGreaterThanFullViewportWidth = (leftViewportWidthMinusCurrentCol + newColumnWidth) > viewportFullWidth; + + if (isGreaterThanFullViewportWidth) { + const resizeWidthToRemoveFromExceededWidthReadjustment = this.resizeByContentOptions.widthToRemoveFromExceededWidthReadjustment ?? 50; + adjustedWidth = (leftViewportWidth - leftViewportWidthMinusCurrentCol + rightViewportWidth - resizeWidthToRemoveFromExceededWidthReadjustment); + } + } + return Math.ceil(adjustedWidth); } } diff --git a/src/aurelia-slickgrid/tsconfig.build.json b/src/aurelia-slickgrid/tsconfig.build.json index 5b8b6f56f..842c8eaa0 100644 --- a/src/aurelia-slickgrid/tsconfig.build.json +++ b/src/aurelia-slickgrid/tsconfig.build.json @@ -1,10 +1,10 @@ { "compilerOptions": { - "module": "amd", + "module": "esnext", "moduleResolution": "node", - "target": "es5", + "target": "es2018", "lib": [ - "es2017", + "es2018", "dom" ], "typeRoots": [ diff --git a/src/examples/slickgrid/example27.ts b/src/examples/slickgrid/example27.ts index 8882a7923..0e2ca8383 100644 --- a/src/examples/slickgrid/example27.ts +++ b/src/examples/slickgrid/example27.ts @@ -198,6 +198,10 @@ export class Example27 { this.aureliaGrid.treeDataService.toggleTreeDataCollapse(false); } + dynamicallyChangeFilter() { + this.aureliaGrid.filterService.updateFilters([{ columnId: 'percentComplete', operator: '<', searchTerms: [40] }]); + } + logHierarchicalStructure() { console.log('exploded array', this.aureliaGrid.treeDataService.datasetHierarchical /* , JSON.stringify(explodedArray, null, 2) */); } diff --git a/src/examples/slickgrid/example32.ts b/src/examples/slickgrid/example32.ts index 7f6305a65..658b97f50 100644 --- a/src/examples/slickgrid/example32.ts +++ b/src/examples/slickgrid/example32.ts @@ -342,8 +342,10 @@ export class Example32 { enableAutoResizeColumnsByCellContent: true, // optional resize calculation options - resizeDefaultRatioForStringType: 0.92, - resizeFormatterPaddingWidthInPx: 8, // optional editor formatter padding for resize calculation + resizeByContentOptions: { + defaultRatioForStringType: 0.92, + formatterPaddingWidthInPx: 8, // optional editor formatter padding for resize calculation + }, enableExcelExport: true, excelExportOptions: { diff --git a/test/cypress/integration/example01.spec.js b/test/cypress/integration/example01.spec.js index 95ab1e816..1616e34b8 100644 --- a/test/cypress/integration/example01.spec.js +++ b/test/cypress/integration/example01.spec.js @@ -41,7 +41,7 @@ describe('Example 1 - Basic Grids', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(2)') + .children('.slick-header-menuitem:nth-child(4)') .children('.slick-header-menucontent') .should('contain', 'Sort Descending') .click(); @@ -66,7 +66,7 @@ describe('Example 1 - Basic Grids', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(1)') + .children('.slick-header-menuitem:nth-child(3)') .children('.slick-header-menucontent') .should('contain', 'Sort Ascending') .click(); @@ -94,7 +94,7 @@ describe('Example 1 - Basic Grids', () => { cy.get('#grid2') .find('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(2)') + .children('.slick-header-menuitem:nth-child(4)') .click(); cy.get('#grid2') diff --git a/test/cypress/integration/example03.spec.js b/test/cypress/integration/example03.spec.js index 33fa16f24..3c0d84d35 100644 --- a/test/cypress/integration/example03.spec.js +++ b/test/cypress/integration/example03.spec.js @@ -151,7 +151,7 @@ describe('Example 3 - Grid with Editors', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') + .children('.slick-header-menuitem:nth-child(6)') .children('.slick-header-menucontent') .should('contain', 'Remove Filter') .click(); diff --git a/test/cypress/integration/example06.spec.js b/test/cypress/integration/example06.spec.js index 8cb95c0bf..aeb05f81b 100644 --- a/test/cypress/integration/example06.spec.js +++ b/test/cypress/integration/example06.spec.js @@ -177,7 +177,7 @@ describe('Example 6 - GraphQL Grid', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') + .children('.slick-header-menuitem:nth-child(6)') .children('.slick-header-menucontent') .should('contain', 'Remove Filter') .click(); @@ -207,7 +207,7 @@ describe('Example 6 - GraphQL Grid', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') + .children('.slick-header-menuitem:nth-child(6)') .children('.slick-header-menucontent') .should('contain', 'Remove Filter') .click(); @@ -238,7 +238,7 @@ describe('Example 6 - GraphQL Grid', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') + .children('.slick-header-menuitem:nth-child(6)') .children('.slick-header-menucontent') .should('contain', 'Remove Filter') .click(); @@ -413,27 +413,27 @@ describe('Example 6 - GraphQL Grid', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(1)') + .children('.slick-header-menuitem:nth-child(3)') .children('.slick-header-menucontent') .should('contain', 'Sort Ascending'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(2)') + .children('.slick-header-menuitem:nth-child(4)') .children('.slick-header-menucontent') .should('contain', 'Sort Descending'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(4)') + .children('.slick-header-menuitem:nth-child(6)') .children('.slick-header-menucontent') .should('contain', 'Remove Filter'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(5)') + .children('.slick-header-menuitem:nth-child(7)') .children('.slick-header-menucontent') .should('contain', 'Remove Sort'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(6)') + .children('.slick-header-menuitem:nth-child(8)') .children('.slick-header-menucontent') .should('contain', 'Hide Column'); }); @@ -521,27 +521,27 @@ describe('Example 6 - GraphQL Grid', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(1)') + .children('.slick-header-menuitem:nth-child(3)') .children('.slick-header-menucontent') .should('contain', 'Trier par ordre croissant'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(2)') + .children('.slick-header-menuitem:nth-child(4)') .children('.slick-header-menucontent') .should('contain', 'Trier par ordre décroissant'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(4)') + .children('.slick-header-menuitem:nth-child(6)') .children('.slick-header-menucontent') .should('contain', 'Supprimer le filtre'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(5)') + .children('.slick-header-menuitem:nth-child(7)') .children('.slick-header-menucontent') .should('contain', 'Supprimer le tri'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(6)') + .children('.slick-header-menuitem:nth-child(8)') .children('.slick-header-menucontent') .should('contain', 'Cacher la colonne'); }); diff --git a/test/cypress/integration/example09.spec.js b/test/cypress/integration/example09.spec.js index 28ceae5e6..10f58c89b 100644 --- a/test/cypress/integration/example09.spec.js +++ b/test/cypress/integration/example09.spec.js @@ -59,7 +59,7 @@ describe('Example 9 - Grid Menu', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(2)') + .children('.slick-header-menuitem:nth-child(4)') .children('.slick-header-menucontent') .should('contain', 'Hide Column') .click({ force: true }); @@ -193,7 +193,7 @@ describe('Example 9 - Grid Menu', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(2)') + .children('.slick-header-menuitem:nth-child(4)') .children('.slick-header-menucontent') .should('contain', 'Hide Column') .click({ force: true }); @@ -264,7 +264,7 @@ describe('Example 9 - Grid Menu', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(2)') + .children('.slick-header-menuitem:nth-child(4)') .children('.slick-header-menucontent') .should('contain', 'Cacher la colonne') .click({ force: true }); @@ -309,7 +309,7 @@ describe('Example 9 - Grid Menu', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(2)') + .children('.slick-header-menuitem:nth-child(4)') .children('.slick-header-menucontent') .should('contain', 'Cacher la colonne') .click({ force: true }); diff --git a/test/cypress/integration/example12.spec.js b/test/cypress/integration/example12.spec.js index c45faf9db..141d8254c 100644 --- a/test/cypress/integration/example12.spec.js +++ b/test/cypress/integration/example12.spec.js @@ -168,6 +168,8 @@ describe('Example 12: Localization (i18n)', () => { .invoke('val', 25) .trigger('change'); + cy.wait(50); + cy.get('#grid12') .find('.slick-row') .each(($row, index) => { @@ -229,7 +231,7 @@ describe('Example 12: Localization (i18n)', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(2)') + .children('.slick-header-menuitem:nth-child(4)') .children('.slick-header-menucontent') .should('contain', 'Sort Descending') .click(); diff --git a/test/cypress/integration/example15.spec.js b/test/cypress/integration/example15.spec.js index c498e3c23..d65408635 100644 --- a/test/cypress/integration/example15.spec.js +++ b/test/cypress/integration/example15.spec.js @@ -178,7 +178,7 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') + .children('.slick-header-menuitem:nth-child(5)') .children('.slick-header-menucontent') .should('contain', 'Sort Descending') .click(); @@ -405,7 +405,7 @@ describe('Example 15: Grid State & Presets using Local Storage', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(8)') + .children('.slick-header-menuitem:nth-child(9)') .children('.slick-header-menucontent') .should('contain', 'Cacher la colonne') .click(); diff --git a/test/cypress/integration/example16.spec.js b/test/cypress/integration/example16.spec.js index fecf0c812..59f7b731a 100644 --- a/test/cypress/integration/example16.spec.js +++ b/test/cypress/integration/example16.spec.js @@ -171,7 +171,7 @@ describe('Example 16 - Row Move & Checkbox Selector Selector Plugins', () => { }); it('should be able to toggle Sorting functionality (disable) and expect all header menu Sorting commands to be hidden and also not show Sort hint while hovering a column', () => { - const expectedFullHeaderMenuCommands = ['Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column']; + const expectedFullHeaderMenuCommands = ['Resize by Content', '', 'Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column']; cy.get('.slick-sort-indicator').should('have.length.greaterThan', 0); // sort icon hints cy.get('[data-test="toggle-sorting-btn"]').click(); // disable it @@ -220,7 +220,7 @@ describe('Example 16 - Row Move & Checkbox Selector Selector Plugins', () => { }); it('should be able to toggle Sorting functionality (re-enable) and expect all Sorting header menu commands to be hidden and also not show Sort hint while hovering a column', () => { - const expectedFullHeaderMenuCommands = ['Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column']; + const expectedFullHeaderMenuCommands = ['Resize by Content', '', 'Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column']; cy.get('.slick-sort-indicator').should('have.length', 0); // sort icon hints cy.get('[data-test="toggle-sorting-btn"]').click(); // enable it back @@ -265,7 +265,7 @@ describe('Example 16 - Row Move & Checkbox Selector Selector Plugins', () => { }); it('should be able to click disable Sorting functionality button and expect all Sorting commands to be hidden and also not show Sort hint while hovering a column', () => { - const expectedFullHeaderMenuCommands = ['Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column']; + const expectedFullHeaderMenuCommands = ['Resize by Content', '', 'Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column']; cy.get('.slick-sort-indicator').should('have.length.greaterThan', 0); // sort icon hints cy.get('[data-test="disable-sorting-btn"]').click().click(); // even clicking twice should have same result @@ -293,7 +293,7 @@ describe('Example 16 - Row Move & Checkbox Selector Selector Plugins', () => { }); it('should be able to click disable Filter functionality button and expect all Filter commands to be hidden and also not show Sort hint while hovering a column', () => { - const expectedFullHeaderMenuCommands = ['Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column']; + const expectedFullHeaderMenuCommands = ['Resize by Content', '', 'Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column']; cy.get('[data-test="disable-filters-btn"]').click().click(); // even clicking twice should have same result diff --git a/test/cypress/integration/example19.spec.js b/test/cypress/integration/example19.spec.js index 30872a4ac..de355aa2f 100644 --- a/test/cypress/integration/example19.spec.js +++ b/test/cypress/integration/example19.spec.js @@ -201,7 +201,7 @@ describe('Example 19 - Row Detail View', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(2)') + .children('.slick-header-menuitem:nth-child(4)') .children('.slick-header-menucontent') .should('contain', 'Sort Descending') .click(); @@ -215,7 +215,7 @@ describe('Example 19 - Row Detail View', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(1)') + .children('.slick-header-menuitem:nth-child(3)') .children('.slick-header-menucontent') .should('contain', 'Sort Ascending') .click(); diff --git a/test/cypress/integration/example20.spec.js b/test/cypress/integration/example20.spec.js index 9bd0f5866..ab63ad943 100644 --- a/test/cypress/integration/example20.spec.js +++ b/test/cypress/integration/example20.spec.js @@ -108,7 +108,7 @@ describe('Example 20 - Frozen Grid', () => { cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(7)') + .children('.slick-header-menuitem:nth-child(8)') .children('.slick-header-menucontent') .should('contain', 'Hide Column') .click(); diff --git a/test/cypress/integration/example32.spec.js b/test/cypress/integration/example32.spec.js index fe8fba3ce..adbe1eed0 100644 --- a/test/cypress/integration/example32.spec.js +++ b/test/cypress/integration/example32.spec.js @@ -9,33 +9,33 @@ describe('Example 32 - Columns Resize by Content', () => { }); it('should have cell that fit the text content', () => { - cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('equal', 75); - cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('equal', 67); - cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('equal', 59); - cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('equal', 102); - cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('equal', 97); - cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('equal', 72); - cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('equal', 71); - cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('equal', 72); - cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('equal', 179); - cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('equal', 94); - cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('equal', 58); + cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('be.greaterThan', 75); + cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('be.greaterThan', 67); + cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('be.greaterThan', 59); + cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.greaterThan', 102); + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.greaterThan', 97); + cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('be.greaterThan', 72); + cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('be.greaterThan', 71); + cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('be.greaterThan', 72); + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.greaterThan', 179); + cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('be.greaterThan', 94); + cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('be.greaterThan', 57); }); it('should make the grid readonly and expect to fit the text by content and expect column width to be the same as earlier', () => { cy.get('[data-test="toggle-readonly-btn"]').click(); - cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('equal', 75); - cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('equal', 67); - cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('equal', 59); - cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('equal', 102); - cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('equal', 97); - cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('equal', 72); - cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('equal', 71); - cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('equal', 72); - cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('equal', 179); - cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('equal', 94); - cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('equal', 58); + cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('be.greaterThan', 75); + cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('be.greaterThan', 67); + cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('be.greaterThan', 59); + cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.greaterThan', 102); + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.greaterThan', 97); + cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('be.greaterThan', 72); + cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('be.greaterThan', 71); + cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('be.greaterThan', 72); + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.greaterThan', 179); + cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('be.greaterThan', 94); + cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('be.greaterThan', 57); }); it('should click on (default resize "autosizeColumns") and expect column to be much thinner and fit all its column within the grid container', () => { @@ -53,4 +53,33 @@ describe('Example 32 - Columns Resize by Content', () => { cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('be.lt', 100); cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('equal', 58); }); + + it('should double-click on the "Complexity" column resize handle and expect the column to become wider and show all text', () => { + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.lt', 80); + + cy.get('.slick-header-column:nth-child(6) .slick-resizable-handle') + .dblclick(); + + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.gt', 95); + }); + + it('should open the "Product" header menu and click on "Resize by Content" and expect the column to become wider and show all text', () => { + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.lt', 120); + + cy.get('#grid32') + .find('.slick-header-column:nth-child(10)') + .trigger('mouseover') + .children('.slick-header-menubutton') + .invoke('show') + .click(); + + cy.get('.slick-header-menu') + .should('be.visible') + .children('.slick-header-menuitem:nth-child(1)') + .children('.slick-header-menucontent') + .should('contain', 'Resize by Content') + .click(); + + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.gt', 120); + }); }); diff --git a/test/tsconfig.spec.json b/test/tsconfig.spec.json index fc9782e9b..85f29b54a 100644 --- a/test/tsconfig.spec.json +++ b/test/tsconfig.spec.json @@ -3,6 +3,11 @@ "compilerOptions": { "outDir": "../out-tsc/spec", "baseUrl": "./", + "target": "es2018", + "module": "commonjs", + "lib": [ + "es2018" + ], "types": [ "cypress", "jest", diff --git a/tsconfig.json b/tsconfig.json index bc50b3e0d..b6e4b4556 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compileOnSave": false, "compilerOptions": { - "target": "es5", + "target": "es2018", "module": "esnext", "sourceMap": true, "downlevelIteration": true, @@ -13,7 +13,7 @@ "skipLibCheck": true, "strict": true, "lib": [ - "es2017", + "es2018", "dom" ], "typeRoots": [ diff --git a/yarn.lock b/yarn.lock index b7e3a23b8..697f65d98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -778,10 +778,10 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@slickgrid-universal/common@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@slickgrid-universal/common/-/common-0.14.0.tgz#10b23faedcb0b0376311852d824e9e172d4730a8" - integrity sha512-JQwhOiK7ca8R6laDFvktKsSwKRGjLaTYVfvpwz4k8YyvMqOhTQvv2To7cknu5s1F+FV7ExUJc7FyOm3T2+6nww== +"@slickgrid-universal/common@^0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/common/-/common-0.14.1.tgz#d97476cb343d547b2612c12249ae4ec91f197011" + integrity sha512-kxErQgNTpQ1Hhcen2cWMGBDmMEApEuBr5MCWvinDc1tzjKW1rzCCKMbxXbz9KTjfaaeGTIvEO5x1Ny8tfn2Tpw== dependencies: assign-deep "^1.0.1" dequal "^2.0.2" @@ -794,60 +794,60 @@ slickgrid "^2.4.36" un-flatten-tree "^2.0.12" -"@slickgrid-universal/composite-editor-component@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@slickgrid-universal/composite-editor-component/-/composite-editor-component-0.14.0.tgz#72cd31fedb871dfbd75336f38312eb2427082a3d" - integrity sha512-Vdcj2mixG17eWyEB5YBoENNzkys2jGZtl+i/0PLGaT/gyDO7UAWY4xOCGl11TZ0VPTC0/J7G3xnhsYHuArUlfg== +"@slickgrid-universal/composite-editor-component@^0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/composite-editor-component/-/composite-editor-component-0.14.1.tgz#db01e1c62e0f5a7894f6e8580d81b558f59b0d70" + integrity sha512-ezNXiMxMeoZF27IDkzjHKiSxHx2YlbV04kFiLboGjQCK83nkAkNZd0ZXBJrhO7zzTG6ZQdCU3Kei/uXLJYOHVg== dependencies: - "@slickgrid-universal/common" "^0.14.0" + "@slickgrid-universal/common" "^0.14.1" assign-deep "^1.0.1" dompurify "^2.2.8" -"@slickgrid-universal/empty-warning-component@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@slickgrid-universal/empty-warning-component/-/empty-warning-component-0.14.0.tgz#0b86c90ab39810157d958c7782f00b5b8942d0b7" - integrity sha512-9a5lmwUidv9Kv+IWRaaSxeo8AgFX4wAseAMEVA2qw8G1w3+jJUTPegDqfA6E6kdH3/QU6hi9NnMDdotS+l1ZFA== +"@slickgrid-universal/empty-warning-component@^0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/empty-warning-component/-/empty-warning-component-0.14.1.tgz#a1e5dfa3a6945205a48ba8e7e101515dc58d8555" + integrity sha512-W8+G8AAY52huo1sfoJMDHZexzmgsgiahyES41bzqE9uV+jjhIRFUZSkttikrVYyBUmm+4Q71bLH+F2gVtrxv4Q== dependencies: - "@slickgrid-universal/common" "^0.14.0" + "@slickgrid-universal/common" "^0.14.1" dompurify "^2.2.8" -"@slickgrid-universal/excel-export@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@slickgrid-universal/excel-export/-/excel-export-0.14.0.tgz#c85a51b28affb8dc47c1444e53bfb424f275ce43" - integrity sha512-sAu+Mj4wKGd8HmLeblYt5cMCxRErgeOWekwhKU2udOCtxGqvO65vZ7qmaHHXh4ZFXtFX0sEOeckc9F+Lpxi9Lw== +"@slickgrid-universal/excel-export@^0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/excel-export/-/excel-export-0.14.1.tgz#785c064c005e71a8e75de6ff466341af987fd6af" + integrity sha512-1GEDRxmjCjBK48Z2U7YY9KmQFRuQGhqpqtzFoyRDwyvS3LyAlHh7u6DYg8Rs03Tp8wo8efyMB5hi65T0G5Y/tA== dependencies: - "@slickgrid-universal/common" "^0.14.0" + "@slickgrid-universal/common" "^0.14.1" excel-builder-webpacker "^2.1.1" moment-mini "^2.24.0" -"@slickgrid-universal/graphql@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@slickgrid-universal/graphql/-/graphql-0.14.0.tgz#9457dad2d069feb81caa445c542407ab581dda51" - integrity sha512-3WHgPhTyG5XYehHuVFUXfJnHB5Xk6lKUpoLlYqGizNTsnA/uRDmRldWLbTRC49gCwEe/qmS95+lq6QCcYcBxEg== +"@slickgrid-universal/graphql@^0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/graphql/-/graphql-0.14.1.tgz#6e1152b549f1dac45ffb08d936c2b97a3fcf69ad" + integrity sha512-1ivA3XUcUC3c+vuWJ7eAVJwfhPIh7+yqxbLytFhdglyIj5xe45HFNG34IVgSmv+4JDf6U5Go/nXNqYrFl72Zpg== dependencies: - "@slickgrid-universal/common" "^0.14.0" + "@slickgrid-universal/common" "^0.14.1" -"@slickgrid-universal/odata@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@slickgrid-universal/odata/-/odata-0.14.0.tgz#46fba6d187b8c30380fb8ba294ba289dd1964483" - integrity sha512-0L4X1M1yj+Gg8UrOTS+T6sMqwGwyFL3qCuBQhhXIHUFebAW1bBn4hTczV5ojGeIA01Hz0uSHhOi3ygs8rQeQ1A== +"@slickgrid-universal/odata@^0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/odata/-/odata-0.14.1.tgz#d3b88df4db81a1717c1e00ce39f9301032f76bbf" + integrity sha512-Sj/Z3Fmui/Pcq8b69l6qL2Ilu7OUK+HLLimakVqjz6Tds+9ocLZxPRvyAEqz5354uSWA0QiL7QKzvPgK5jfdTg== dependencies: - "@slickgrid-universal/common" "^0.14.0" + "@slickgrid-universal/common" "^0.14.1" -"@slickgrid-universal/rxjs-observable@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@slickgrid-universal/rxjs-observable/-/rxjs-observable-0.14.0.tgz#836eeccf45f4cd6db9ef96976c1393595c1a09db" - integrity sha512-AwDupgFitB92moE3rTNyjH4AWBRtHDiXpdbEbABJIOZ6ACHudWAYBJkkHcYKLkE3Fln68lNWgK6BN7HfcKKypQ== +"@slickgrid-universal/rxjs-observable@^0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/rxjs-observable/-/rxjs-observable-0.14.1.tgz#d0752723d2266ec78643648f45ef5555c7dec270" + integrity sha512-ktFpdjs++FstPA/WlbP1+IKfywIkvdkX6YY3nSo7jOvEBTGKZ4+06xbzMpcx45I/2rx/zaD4PkOluPUKyUePqw== dependencies: - "@slickgrid-universal/common" "^0.14.0" + "@slickgrid-universal/common" "^0.14.1" rxjs "^7.0.1" -"@slickgrid-universal/text-export@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@slickgrid-universal/text-export/-/text-export-0.14.0.tgz#79a4937dce0d5aa994433203a7e40b96e0486580" - integrity sha512-Vxdtfkse2qij7Woj3FP1gFAzCWuxST8jBLyvKe9IPb9VK0SMrmRVIxkRU4O7v9T+sYBb0trrVwcplWKT4ugmXA== +"@slickgrid-universal/text-export@^0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/text-export/-/text-export-0.14.1.tgz#4535b12c0039180943e96d1519016077fad2589b" + integrity sha512-NPFNeuFTPShi8Ac7HgDSczLc7dcANLKuHUWB7eGm8SeqO/swlLMYTaC0x5CqavQ9DLfvHjfYS/f0TZRYVUuWLg== dependencies: - "@slickgrid-universal/common" "^0.14.0" + "@slickgrid-universal/common" "^0.14.1" text-encoding-utf-8 "^1.0.2" "@tootallnate/once@1":