diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts
index b9c66a092..44173418f 100644
--- a/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts
+++ b/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts
@@ -123,10 +123,7 @@ export class Example5 {
columnId: 'title',
parentPropName: 'parentId',
// this is optional, you can define the tree level property name that will be used for the sorting/indentation, internally it will use "__treeLevel"
- // levelPropName: 'indent',
-
- // you can add an optional prefix to all the child values
- indentedChildValuePrefix: '',
+ levelPropName: 'treeLevel',
indentMarginLeft: 15,
// you can optionally sort by a different column and/or sort direction
@@ -134,7 +131,15 @@ export class Example5 {
initialSort: {
columnId: 'title',
direction: 'ASC'
- }
+ },
+ // we can also add a custom Formatter just for the title text portion
+ titleFormatter: (_row, _cell, value, _def, dataContext) => {
+ let prefix = '';
+ if (dataContext.treeLevel > 0) {
+ prefix = ``;
+ }
+ return `${prefix}${value}(parentId: ${dataContext.parentId})`;
+ },
},
multiColumnSort: false, // multi-column sorting is not supported with Tree Data, so you need to disable it
presets: {
@@ -152,7 +157,7 @@ export class Example5 {
addNewRow() {
const newId = this.sgb.dataset.length;
const parentPropName = 'parentId';
- const treeLevelPropName = '__treeLevel'; // if undefined in your options, the default prop name is "__treeLevel"
+ const treeLevelPropName = 'treeLevel'; // if undefined in your options, the default prop name is "__treeLevel"
const newTreeLevel = 1;
// find first parent object and add the new item as a child
const childItemFound = this.sgb.dataset.find((item) => item[treeLevelPropName] === newTreeLevel);
@@ -162,7 +167,7 @@ export class Example5 {
const newItem = {
id: newId,
parentId: parentItemFound.id,
- title: this.formatTitle(newId, parentItemFound.id),
+ title: `Task ${newId}`,
duration: '1 day',
percentComplete: 99,
start: new Date(),
@@ -227,7 +232,7 @@ export class Example5 {
item['id'] = i;
item['parentId'] = parentId;
- item['title'] = this.formatTitle(i, parentId);
+ item['title'] = `Task ${i}`;
item['duration'] = '5 days';
item['percentComplete'] = Math.round(Math.random() * 100);
item['start'] = new Date(randomYear, randomMonth, randomDay);
@@ -239,8 +244,4 @@ export class Example5 {
}
return data;
}
-
- formatTitle(taskId: number, parentId: number) {
- return `Task ${taskId} (parentId: ${parentId})`;
- }
}
diff --git a/packages/common/src/formatters/__tests__/treeFormatter.spec.ts b/packages/common/src/formatters/__tests__/treeFormatter.spec.ts
index 5fabb09d0..608ed53ee 100644
--- a/packages/common/src/formatters/__tests__/treeFormatter.spec.ts
+++ b/packages/common/src/formatters/__tests__/treeFormatter.spec.ts
@@ -26,7 +26,7 @@ describe('Tree Formatter', () => {
{ id: 5, firstName: 'Sponge', lastName: 'Bob', fullName: 'Sponge Bob', email: 'sponge.bob@cartoon.com', address: { zip: 888888 }, parentId: 2, indent: 3, __collapsed: true },
];
mockGridOptions = {
- treeDataOptions: { levelPropName: 'indent', indentedChildValuePrefix: '' }
+ treeDataOptions: { levelPropName: 'indent' }
} as GridOption;
jest.spyOn(gridStub, 'getOptions').mockReturnValue(mockGridOptions);
});
@@ -62,7 +62,10 @@ describe('Tree Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[0]);
const output = treeFormatter(1, 1, dataset[0]['firstName'], {} as Column, dataset[0], gridStub);
- expect(output).toBe(`John`);
+ expect(output).toEqual({
+ addClasses: 'slick-tree-level-0',
+ text: `John`
+ });
});
it('should return a span without any icon and 15px indentation of a tree level 1', () => {
@@ -71,7 +74,10 @@ describe('Tree Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);
const output = treeFormatter(1, 1, dataset[1]['firstName'], {} as Column, dataset[1], gridStub);
- expect(output).toBe(`Jane`);
+ expect(output).toEqual({
+ addClasses: 'slick-tree-level-1',
+ text: `Jane`
+ });
});
it('should return a span without any icon and 30px indentation of a tree level 2', () => {
@@ -80,7 +86,10 @@ describe('Tree Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);
const output = treeFormatter(1, 1, dataset[2]['firstName'], {} as Column, dataset[2], gridStub);
- expect(output).toBe(`Bob`);
+ expect(output).toEqual({
+ addClasses: 'slick-tree-level-2',
+ text: `Bob`
+ });
});
it('should return a span with expanded icon and 15px indentation of a tree level 1 when current item is greater than next item', () => {
@@ -89,7 +98,10 @@ describe('Tree Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[2]);
const output = treeFormatter(1, 1, dataset[1]['firstName'], {} as Column, dataset[1], gridStub);
- expect(output).toBe(`Jane`);
+ expect(output).toEqual({
+ addClasses: 'slick-tree-level-1',
+ text: `Jane`
+ });
});
it('should return a span with collapsed icon and 0px indentation of a tree level 0 when current item is lower than next item', () => {
@@ -98,27 +110,29 @@ describe('Tree Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);
const output = treeFormatter(1, 1, dataset[3]['firstName'], {} as Column, dataset[3], gridStub);
- expect(output).toBe(`Barbara`);
+ expect(output).toEqual({
+ addClasses: 'slick-tree-level-0',
+ text: `Barbara`
+ });
});
it('should return a span with expanded icon and 15px indentation of a tree level 1 with a value prefix when provided', () => {
- mockGridOptions.treeDataOptions.indentedChildValuePrefix = '';
+ mockGridOptions.treeDataOptions.levelPropName = 'indent';
+ mockGridOptions.treeDataOptions.titleFormatter = (_row, _cell, value, _def, dataContext) => {
+ if (dataContext.indent > 0) {
+ return `${value}`;
+ }
+ return value || '';
+ };
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
jest.spyOn(dataViewStub, 'getIdxById').mockReturnValue(1);
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[2]);
- const output = treeFormatter(1, 1, dataset[1]['firstName'], {} as Column, dataset[1], gridStub);
- expect(output).toBe(`Jane`);
- });
-
- it('should return a span with collapsed icon and 30px indentation of a tree level 2 when current item is lower than next item', () => {
- mockGridOptions.treeDataOptions.indentedChildValuePrefix = '';
- jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
- jest.spyOn(dataViewStub, 'getIdxById').mockReturnValue(1);
- jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[5]);
-
- const output = treeFormatter(1, 1, dataset[2]['firstName'], {} as Column, dataset[2], gridStub);
- expect(output).toBe(`Bob`);
+ const output = treeFormatter(1, 1, { ...dataset[1]['firstName'], indent: 1 }, { field: 'firstName' } as Column, dataset[1], gridStub);
+ expect(output).toEqual({
+ addClasses: 'slick-tree-level-1',
+ text: `Jane`
+ });
});
it('should execute "queryFieldNameGetterFn" callback to get field name to use when it is defined', () => {
@@ -128,7 +142,10 @@ describe('Tree Formatter', () => {
const mockColumn = { id: 'firstName', field: 'firstName', queryFieldNameGetterFn: () => 'fullName' } as Column;
const output = treeFormatter(1, 1, null, mockColumn as Column, dataset[3], gridStub);
- expect(output).toBe(`Barbara Cane`);
+ expect(output).toEqual({
+ addClasses: 'slick-tree-level-0',
+ text: `Barbara Cane`
+ });
});
it('should execute "queryFieldNameGetterFn" callback to get field name and also apply html encoding when output value includes a character that should be encoded', () => {
@@ -138,7 +155,10 @@ describe('Tree Formatter', () => {
const mockColumn = { id: 'firstName', field: 'firstName', queryFieldNameGetterFn: () => 'fullName' } as Column;
const output = treeFormatter(1, 1, null, mockColumn as Column, dataset[4], gridStub);
- expect(output).toBe(`Anonymous < Doe`);
+ expect(output).toEqual({
+ addClasses: 'slick-tree-level-0',
+ text: `Anonymous < Doe`
+ });
});
it('should execute "queryFieldNameGetterFn" callback to get field name, which has (.) dot notation reprensenting complex object', () => {
@@ -148,6 +168,9 @@ describe('Tree Formatter', () => {
const mockColumn = { id: 'zip', field: 'zip', queryFieldNameGetterFn: () => 'address.zip' } as Column;
const output = treeFormatter(1, 1, null, mockColumn as Column, dataset[3], gridStub);
- expect(output).toBe(`444444`);
+ expect(output).toEqual({
+ addClasses: 'slick-tree-level-0',
+ text: `444444`
+ });
});
});
diff --git a/packages/common/src/formatters/treeFormatter.ts b/packages/common/src/formatters/treeFormatter.ts
index ee70645a9..512ef06b8 100644
--- a/packages/common/src/formatters/treeFormatter.ts
+++ b/packages/common/src/formatters/treeFormatter.ts
@@ -1,13 +1,15 @@
import { SlickDataView, Formatter } from './../interfaces/index';
import { getDescendantProperty, sanitizeTextByAvailableSanitizer } from '../services/utilities';
+import { parseFormatterWhenExist } from '../services';
/** Formatter that must be use with a Tree Data column */
-export const treeFormatter: Formatter = (_row, _cell, value, columnDef, dataContext, grid) => {
+export const treeFormatter: Formatter = (row, cell, value, columnDef, dataContext, grid) => {
const dataView = grid.getData();
const gridOptions = grid.getOptions();
const treeDataOptions = gridOptions?.treeDataOptions;
- const treeLevelPropName = treeDataOptions?.levelPropName ?? '__treeLevel';
+ const collapsedPropName = treeDataOptions?.collapsedPropName ?? '__collapsed';
const indentMarginLeft = treeDataOptions?.indentMarginLeft ?? 15;
+ const treeLevelPropName = treeDataOptions?.levelPropName ?? '__treeLevel';
let outputValue = value;
if (typeof columnDef.queryFieldNameGetterFn === 'function') {
@@ -27,22 +29,25 @@ export const treeFormatter: Formatter = (_row, _cell, value, columnDef, dataCont
}
if (dataView?.getItemByIdx) {
- const sanitizedOutputValue = sanitizeTextByAvailableSanitizer(gridOptions, outputValue);
const identifierPropName = dataView.getIdPropertyName() ?? 'id';
const treeLevel = dataContext[treeLevelPropName] || 0;
- const spacer = ``;
+ const indentSpacer = ``;
const idx = dataView.getIdxById(dataContext[identifierPropName]);
const nextItemRow = dataView.getItemByIdx((idx || 0) + 1);
- const valuePrefix = treeLevel > 0 ? treeDataOptions?.indentedChildValuePrefix ?? '' : '';
+ const slickTreeLevelClass = `slick-tree-level-${treeLevel}`;
+ let toggleClass = '';
if (nextItemRow?.[treeLevelPropName] > treeLevel) {
- if (dataContext.__collapsed) {
- return `${spacer}${valuePrefix}${sanitizedOutputValue}`;
- } else {
- return `${spacer}${valuePrefix}${sanitizedOutputValue}`;
- }
+ toggleClass = dataContext?.[collapsedPropName] ? 'collapsed' : 'expanded'; // parent with child will have a toggle icon
+ }
+
+ if (treeDataOptions?.titleFormatter) {
+ outputValue = parseFormatterWhenExist(treeDataOptions.titleFormatter, row, cell, dataContext, columnDef, grid);
}
- return `${spacer}${valuePrefix}${sanitizedOutputValue}`;
+ const sanitizedOutputValue = sanitizeTextByAvailableSanitizer(gridOptions, outputValue, { ADD_ATTR: ['target'] });
+ const spanToggleClass = `slick-group-toggle ${toggleClass}`.trim();
+ const outputHtml = `${indentSpacer}${sanitizedOutputValue}`;
+ return { addClasses: slickTreeLevelClass, text: outputHtml };
}
return '';
};
diff --git a/packages/common/src/interfaces/treeDataOption.interface.ts b/packages/common/src/interfaces/treeDataOption.interface.ts
index 12fcebd59..b91d32428 100644
--- a/packages/common/src/interfaces/treeDataOption.interface.ts
+++ b/packages/common/src/interfaces/treeDataOption.interface.ts
@@ -1,12 +1,14 @@
-import { Aggregator } from './aggregator.interface';
+// import { Aggregator } from './aggregator.interface';
import { SortDirection, SortDirectionString } from '../enums/index';
+import { Formatter } from './formatter.interface';
export interface TreeDataOption {
/** Column Id of which column in the column definitions has the Tree Data, there can only be one with a Tree Data. */
columnId: string;
/** Grouping Aggregators array */
- aggregators?: Aggregator[];
+ // NOT YET IMPLEMENTED
+ // aggregators?: Aggregator[];
/** Optionally define the initial sort column and direction */
initialSort?: {
@@ -29,9 +31,6 @@ export interface TreeDataOption {
/** Defaults to "__parentId", object property name used to designate the Parent Id */
parentPropName?: string;
- /** Defaults to empty string, add an optional prefix to each of the child values (in other words, add a prefix to all values which have at tree level indentation greater than 0) */
- indentedChildValuePrefix?: string;
-
/** Defaults to "__treeLevel", object property name used to designate the Tree Level depth number */
levelPropName?: string;
@@ -52,4 +51,7 @@ export interface TreeDataOption {
* and if we add a regular character like a dot then it keeps all tree level indentation spaces
*/
exportIndentationLeadingChar?: string;
+
+ /** Optional Title Formatter (allows you to format/style the title text differently) */
+ titleFormatter?: Formatter;
}
diff --git a/packages/common/src/services/export-utilities.ts b/packages/common/src/services/export-utilities.ts
index ed1b2818f..d2d282bfe 100644
--- a/packages/common/src/services/export-utilities.ts
+++ b/packages/common/src/services/export-utilities.ts
@@ -1,7 +1,18 @@
import { Column, ExcelExportOption, Formatter, SlickGrid, TextExportOption } from '../interfaces/index';
+/**
+ * Goes through every possible ways to find and apply a Formatter when found,
+ * it will first check if a `exportCustomFormatter` is defined else it will check if there's a regular `formatter` and `exportWithFormatter` is enabled.
+ * This function is similar to `applyFormatterWhenDefined()` except that it execute any `exportCustomFormatter` while `applyFormatterWhenDefined` does not.
+ * @param {Number} row - grid row index
+ * @param {Number} col - grid column index
+ * @param {Object} dataContext - item data context object
+ * @param {Object} columnDef - column definition
+ * @param {Object} grid - Slick Grid object
+ * @param {Object} exportOptions - Excel or Text Export Options
+ * @returns formatted string output or empty string
+ */
export function exportWithFormatterWhenDefined(row: number, col: number, dataContext: any, columnDef: Column, grid: SlickGrid, exportOptions?: TextExportOption | ExcelExportOption) {
- let output = '';
let isEvaluatingFormatter = false;
// first check if there are any export options provided (as Grid Options)
@@ -14,6 +25,31 @@ export function exportWithFormatterWhenDefined(row: number, col: number, dataCon
isEvaluatingFormatter = !!columnDef.exportWithFormatter;
}
+ let formatter: Formatter | undefined;
+ if (dataContext && columnDef.exportCustomFormatter) {
+ // did the user provide a Custom Formatter for the export
+ formatter = columnDef.exportCustomFormatter;
+ } else if (isEvaluatingFormatter && columnDef.formatter) {
+ // or else do we have a column Formatter AND are we evaluating it?
+ formatter = columnDef.formatter;
+ }
+
+ return parseFormatterWhenExist(formatter, row, col, dataContext, columnDef, grid);
+}
+
+/**
+ * Takes a Formatter function, execute and return the formatted output
+ * @param {Function} formatter - formatter function
+ * @param {Number} row - grid row index
+ * @param {Number} col - grid column index
+ * @param {Object} dataContext - item data context object
+ * @param {Object} columnDef - column definition
+ * @param {Object} grid - Slick Grid object
+ * @returns formatted string output or empty string
+ */
+export function parseFormatterWhenExist(formatter: Formatter | undefined, row: number, col: number, dataContext: any, columnDef: Column, grid: SlickGrid): string {
+ let output = '';
+
// does the field have the dot (.) notation and is a complex object? if so pull the first property name
const fieldId = columnDef.field || columnDef.id || '';
let fieldProperty = fieldId;
@@ -24,15 +60,6 @@ export function exportWithFormatterWhenDefined(row: number, col: number, dataCon
const cellValue = dataContext.hasOwnProperty(fieldProperty) ? dataContext[fieldProperty] : null;
- let formatter: Formatter | undefined;
- if (dataContext && columnDef.exportCustomFormatter) {
- // did the user provide a Custom Formatter for the export
- formatter = columnDef.exportCustomFormatter;
- } else if (isEvaluatingFormatter && columnDef.formatter) {
- // or else do we have a column Formatter AND are we evaluating it?
- formatter = columnDef.formatter;
- }
-
if (typeof formatter === 'function') {
const formattedData = formatter(row, col, cellValue, columnDef, dataContext, grid);
output = formattedData as string;
diff --git a/packages/common/src/services/filter.service.ts b/packages/common/src/services/filter.service.ts
index 78569c82c..bf869346e 100644
--- a/packages/common/src/services/filter.service.ts
+++ b/packages/common/src/services/filter.service.ts
@@ -221,7 +221,7 @@ export class FilterService {
// emit an onFilterChanged event except when it's called by a clear filter
if (!isClearFilterEvent) {
- this.emitFilterChanged(EmitterType.local);
+ await this.emitFilterChanged(EmitterType.local);
}
});
}
@@ -260,7 +260,7 @@ export class FilterService {
}
// emit an event when filter is cleared
- this.emitFilterChanged(emitter);
+ await this.emitFilterChanged(emitter);
return true;
}
@@ -645,7 +645,6 @@ export class FilterService {
} else if (caller === EmitterType.local) {
return this.pubSubService.publish(eventName, this.getCurrentLocalFilters());
}
- return Promise.resolve(true);
}
async onBackendFilterChange(event: KeyboardEvent, args: any) {
@@ -841,7 +840,7 @@ export class FilterService {
}
if (emitChangedEvent) {
- this.emitFilterChanged(emitterType);
+ await this.emitFilterChanged(emitterType);
}
}
return true;
@@ -905,7 +904,7 @@ export class FilterService {
}
if (emitChangedEvent) {
- this.emitFilterChanged(emitterType);
+ await this.emitFilterChanged(emitterType);
}
}
return true;
diff --git a/packages/common/src/services/utilities.ts b/packages/common/src/services/utilities.ts
index 140884d28..48c86446f 100644
--- a/packages/common/src/services/utilities.ts
+++ b/packages/common/src/services/utilities.ts
@@ -887,7 +887,7 @@ export function sanitizeHtmlToText(htmlString: string): string {
* @param dirtyHtml: dirty html string
* @param domPurifyOptions: optional DOMPurify options when using that sanitizer
*/
-export function sanitizeTextByAvailableSanitizer(gridOptions: GridOption, dirtyHtml: string, domPurifyOptions?: DOMPurify.Config & { RETURN_TRUSTED_TYPE: true; }): string {
+export function sanitizeTextByAvailableSanitizer(gridOptions: GridOption, dirtyHtml: string, domPurifyOptions?: DOMPurify.Config): string {
let sanitizedText = dirtyHtml;
if (gridOptions && typeof gridOptions.sanitizer === 'function') {
sanitizedText = gridOptions.sanitizer(dirtyHtml || '');
diff --git a/packages/common/src/styles/slick-bootstrap.scss b/packages/common/src/styles/slick-bootstrap.scss
index 5dcd49af2..9c86eed85 100644
--- a/packages/common/src/styles/slick-bootstrap.scss
+++ b/packages/common/src/styles/slick-bootstrap.scss
@@ -202,12 +202,12 @@
cursor: pointer;
&.expanded:before {
- vertical-align: $icon-group-vertical-align;
display: inline-block;
content: $icon-group-expanded;
font-family: $icon-font-family;
font-size: $icon-group-font-size;
width: $icon-group-width;
+ vertical-align: $icon-group-vertical-align;
}
&.collapsed:before {
@@ -216,6 +216,7 @@
font-family: $icon-font-family;
font-size: $icon-group-font-size;
width: $icon-group-width;
+ vertical-align: $icon-group-vertical-align;
}
}
}
diff --git a/packages/vanilla-bundle/src/services/resizer.service.ts b/packages/vanilla-bundle/src/services/resizer.service.ts
index b5d6909c8..273aeaffc 100644
--- a/packages/vanilla-bundle/src/services/resizer.service.ts
+++ b/packages/vanilla-bundle/src/services/resizer.service.ts
@@ -1,10 +1,10 @@
import {
- exportWithFormatterWhenDefined,
FieldType,
getHtmlElementOffset,
GetSlickEventType,
GridOption,
GridSize,
+ parseFormatterWhenExist,
PubSubService,
sanitizeHtmlToText,
SlickDataView,
@@ -186,8 +186,7 @@ export class ResizerService {
columnDefinitions.forEach((columnDef, colIdx) => {
if (!columnDef.originalWidth) {
const charWidthPx = columnDef?.resizeCharWidthInPx ?? resizeCellCharWidthInPx;
- const exportOptions = this.gridOptions.enableTextExport ? this.gridOptions.exportOptions || this.gridOptions.textExportOptions : this.gridOptions.excelExportOptions;
- const formattedData = exportWithFormatterWhenDefined(rowIdx, colIdx, item, columnDef, this._grid, exportOptions);
+ const formattedData = parseFormatterWhenExist(columnDef?.formatter, rowIdx, colIdx, item, columnDef, this._grid);
const formattedDataSanitized = sanitizeHtmlToText(formattedData);
const formattedTextWidthInPx = Math.ceil(formattedDataSanitized.length * charWidthPx);
const resizeMaxWidthThreshold = columnDef.resizeMaxWidthThreshold;