angular-slickgrid
Version:
Slickgrid components made available in Angular
1,053 lines (1,044 loc) • 123 kB
JavaScript
import { unsubscribeAll, createDomElement, SlickRowSelectionModel, SlickEventData, castObservableToPromise, addToArrayWhenNotExists, FileType, DelimiterType, EventNamingStyle, OperatorType, Filters, SlickEventHandler, SlickgridConfig as SlickgridConfig$1, BackendUtilityService, GridEventService, SharedService, CollectionService, ExtensionUtility, FilterFactory, FilterService, ResizerService, SortService, TreeDataService, PaginationService, ExtensionService, GridStateService, GridService, HeaderGroupingService, emptyElement, SlickGroupItemMetadataProvider, SlickDataView, autoAddEditorFormatterToColumnsWithEditor, SlickGrid, GridStateType, ExtensionName, isColumnDateType } from '@slickgrid-universal/common';
export * from '@slickgrid-universal/common';
import * as i0 from '@angular/core';
import { Injectable, Optional, EventEmitter, ContentChild, Input, Output, Inject, Component, NgModule } from '@angular/core';
import * as i1 from '@ngx-translate/core';
import { TranslateModule } from '@ngx-translate/core';
import { SlickRowDetailView as SlickRowDetailView$1 } from '@slickgrid-universal/row-detail-view-plugin';
import { Observable } from 'rxjs';
import { EventPubSubService } from '@slickgrid-universal/event-pub-sub';
import { SlickEmptyWarningComponent } from '@slickgrid-universal/empty-warning-component';
import { SlickFooterComponent } from '@slickgrid-universal/custom-footer-component';
import { SlickPaginationComponent } from '@slickgrid-universal/pagination-component';
import { RxJsResource } from '@slickgrid-universal/rxjs-observable';
import { extend } from '@slickgrid-universal/utils';
import { dequal } from 'dequal/lite';
import * as i5 from '@angular/common';
import { CommonModule } from '@angular/common';
class AngularUtilService {
vcr;
constructor(vcr) {
this.vcr = vcr;
}
createInteractiveAngularComponent(component, targetElement, data, createCompOptions) {
// Create a component reference from the component
const componentRef = this.vcr.createComponent(component, createCompOptions);
// user could provide data to assign to the component instance
if (componentRef?.instance && data) {
Object.assign(componentRef.instance, data);
}
// Get DOM element from component
let domElem = null;
const viewRef = componentRef.hostView;
if (viewRef && Array.isArray(viewRef.rootNodes) && viewRef.rootNodes[0]) {
domElem = viewRef.rootNodes[0];
// when user provides the DOM element target, we will move the dynamic component into that target (aka portal-ing it)
if (targetElement && domElem) {
targetElement.replaceChildren(componentRef.location.nativeElement);
}
}
return { componentRef, domElement: domElem };
}
/**
* Dynamically create an Angular component, user could also provide optional arguments for target, data & createComponent options
* @param {Component} component
* @param {HTMLElement} [targetElement]
* @param {*} [data]
* @param {CreateComponentOption} [createCompOptions]
* @returns
*/
createAngularComponent(component, targetElement, data, createCompOptions) {
// Create a component reference from the component
const componentRef = this.vcr.createComponent(component, createCompOptions);
// user could provide data to assign to the component instance
if (componentRef?.instance && data) {
Object.assign(componentRef.instance, data);
// NOTE: detectChanges() MUST be done BEFORE returning the DOM element in the next step,
// because if we do it only after returning the rootNodes (domElement) then it won't have the instance data yet
// and we would have to wait an extra cycle to see the result, this basically helps with Example22
componentRef.changeDetectorRef.detectChanges();
}
// Get DOM element from component
let domElem = null;
const viewRef = componentRef.hostView;
// get DOM element from the new dynamic Component, make sure this is read after any data and detectChanges()
if (viewRef && Array.isArray(viewRef.rootNodes) && viewRef.rootNodes[0]) {
domElem = viewRef.rootNodes[0];
// when user provides the DOM element target, we will read the new Component html and use it to replace the target html
if (targetElement && domElem) {
targetElement.innerHTML =
typeof createCompOptions?.sanitizer === 'function'
? createCompOptions.sanitizer(domElem.innerHTML || '')
: domElem.innerHTML;
}
}
return { componentRef, domElement: domElem };
}
/**
* Dynamically create an Angular component and append it to the DOM unless a target element is provided,
* user could also provide other optional arguments for data & createComponent options.
* @param {Component} component
* @param {HTMLElement} [targetElement]
* @param {*} [data]
* @param {CreateComponentOption} [createCompOptions]
* @returns
*/
createAngularComponentAppendToDom(component, targetElement, data, createCompOptions) {
const componentOutput = this.createAngularComponent(component, targetElement, data, createCompOptions);
// Append DOM element to the HTML element specified
if (targetElement?.replaceChildren) {
targetElement.replaceChildren(componentOutput.domElement);
}
else {
document.body.appendChild(componentOutput.domElement); // when no target provided, we'll simply add it to the HTML Body
}
return componentOutput;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AngularUtilService, deps: [{ token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AngularUtilService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AngularUtilService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i0.ViewContainerRef }] });
class ContainerService {
dependencies = [];
get(key) {
const dependency = this.dependencies.find((dep) => dep.key === key);
if (dependency?.instance) {
return dependency.instance;
}
return null;
}
dispose() {
this.dependencies = [];
}
registerInstance(key, instance) {
const dependency = this.dependencies.some((dep) => dep.key === key);
if (!dependency) {
this.dependencies.push({ key, instance });
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ContainerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ContainerService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ContainerService, decorators: [{
type: Injectable
}] });
/**
* This is a Translate Service Wrapper for Slickgrid-Universal monorepo lib to work properly,
* it must implement Slickgrid-Universal TranslaterService interface to work properly
*/
class TranslaterService {
translateService;
constructor(translateService) {
this.translateService = translateService;
}
/**
* Method to return the current language used by the App
* @return {string} current language
*/
getCurrentLanguage() {
return this.translateService?.currentLang ?? '';
}
/**
* Method to set the language to use in the App and Translate Service
* @param {string} language
* @return {Promise} output
*/
async use(newLang) {
return this.translateService?.use?.(newLang);
}
/**
* Method which receives a translation key and returns the translated value assigned to that key
* @param {string} translation key
* @return {string} translated value
*/
translate(translationKey) {
return this.translateService?.instant?.(translationKey || ' ');
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslaterService, deps: [{ token: i1.TranslateService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslaterService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TranslaterService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i1.TranslateService, decorators: [{
type: Optional
}] }] });
/**
* Unsubscribe all Observables Subscriptions
* It will return an empty array if it all went well
* @param subscriptions
*/
function unsubscribeAllObservables(subscriptions) {
if (Array.isArray(subscriptions)) {
let subscription = subscriptions.pop();
while (subscription) {
if (typeof subscription.unsubscribe === 'function') {
subscription.unsubscribe();
}
subscription = subscriptions.pop();
}
}
// TODO: deprecated, remove the return type in next major version
return subscriptions;
}
const ROW_DETAIL_CONTAINER_PREFIX = 'container_';
const PRELOAD_CONTAINER_PREFIX = 'container_loading';
class SlickRowDetailView extends SlickRowDetailView$1 {
angularUtilService;
appRef;
eventPubSubService;
gridContainerElement;
rxjs;
rowDetailContainer;
_preloadComponent;
_preloadCompRef;
_views = [];
_viewComponent;
_subscriptions = [];
_userProcessFn;
constructor(angularUtilService, appRef, eventPubSubService, gridContainerElement, rxjs) {
super(eventPubSubService);
this.angularUtilService = angularUtilService;
this.appRef = appRef;
this.eventPubSubService = eventPubSubService;
this.gridContainerElement = gridContainerElement;
this.rxjs = rxjs;
}
get addonOptions() {
return this.getOptions();
}
get datasetIdPropName() {
return this.gridOptions.datasetIdPropertyName || 'id';
}
/** Getter for the Grid Options pulled through the Grid Object */
get gridOptions() {
return (this._grid?.getOptions() || {});
}
get rowDetailViewOptions() {
return this.gridOptions.rowDetailView;
}
addRxJsResource(rxjs) {
this.rxjs = rxjs;
}
/** Dispose of the RowDetailView Extension */
dispose() {
this.disposeAllViewComponents();
this._subscriptions = unsubscribeAll(this._subscriptions); // also unsubscribe all RxJS subscriptions
super.dispose();
}
/** Dispose of all the opened Row Detail Panels Angular View Components */
disposeAllViewComponents() {
do {
const view = this._views.pop();
if (view) {
this.disposeView(view);
}
} while (this._views.length > 0);
}
/** Get the instance of the SlickGrid addon (control or plugin). */
getAddonInstance() {
return this;
}
init(grid) {
this._grid = grid;
super.init(this._grid);
this.register(grid?.getSelectionModel());
}
/**
* Create the plugin before the Grid creation, else it will behave oddly.
* Mostly because the column definitions might change after the grid creation
*/
register(rowSelectionPlugin) {
if (typeof this.gridOptions.rowDetailView?.process === 'function') {
// we need to keep the user "process" method and replace it with our own execution method
// we do this because when we get the item detail, we need to call "onAsyncResponse.notify" for the plugin to work
this._userProcessFn = this.gridOptions.rowDetailView.process; // keep user's process method
this.addonOptions.process = (item) => this.onProcessing(item); // replace process method & run our internal one
}
else {
throw new Error('[Angular-Slickgrid] You need to provide a "process" function for the Row Detail Extension to work properly');
}
if (this._grid && this.gridOptions?.rowDetailView) {
// load the Preload & RowDetail Templates (could be straight HTML or Angular View/ViewModel)
// when those are Angular View/ViewModel, we need to create View Component & provide the html containers to the Plugin (preTemplate/postTemplate methods)
if (!this.gridOptions.rowDetailView.preTemplate) {
this._preloadComponent = this.gridOptions?.rowDetailView?.preloadComponent;
this.addonOptions.preTemplate = () => createDomElement('div', { className: `${PRELOAD_CONTAINER_PREFIX}` });
}
if (!this.gridOptions.rowDetailView.postTemplate) {
this._viewComponent = this.gridOptions?.rowDetailView?.viewComponent;
this.addonOptions.postTemplate = (itemDetail) => createDomElement('div', { className: `${ROW_DETAIL_CONTAINER_PREFIX}${itemDetail[this.datasetIdPropName]}` });
}
// this also requires the Row Selection Model to be registered as well
if (!rowSelectionPlugin || !this._grid.getSelectionModel()) {
rowSelectionPlugin = new SlickRowSelectionModel(this.gridOptions.rowSelectionOptions || { selectActiveRow: true });
this._grid.setSelectionModel(rowSelectionPlugin);
}
// hook all events
if (this._grid && this.rowDetailViewOptions) {
if (this.rowDetailViewOptions.onExtensionRegistered) {
this.rowDetailViewOptions.onExtensionRegistered(this);
}
this.eventHandler.subscribe(this.onAsyncResponse, (event, args) => {
if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onAsyncResponse === 'function') {
this.rowDetailViewOptions.onAsyncResponse(event, args);
}
});
this.eventHandler.subscribe(this.onAsyncEndUpdate, (e, args) => {
// destroy preload if exists
this._preloadCompRef?.destroy();
// triggers after backend called "onAsyncResponse.notify()"
this.renderViewModel(args?.item);
if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onAsyncEndUpdate === 'function') {
this.rowDetailViewOptions.onAsyncEndUpdate(e, args);
}
});
this.eventHandler.subscribe(this.onAfterRowDetailToggle, (e, args) => {
// display preload template & re-render all the other Detail Views after toggling
// the preload View will eventually go away once the data gets loaded after the "onAsyncEndUpdate" event
this.renderPreloadView();
if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onAfterRowDetailToggle === 'function') {
this.rowDetailViewOptions.onAfterRowDetailToggle(e, args);
}
});
this.eventHandler.subscribe(this.onBeforeRowDetailToggle, (e, args) => {
// before toggling row detail, we need to create View Component if it doesn't exist
this.handleOnBeforeRowDetailToggle(e, args);
if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onBeforeRowDetailToggle === 'function') {
return this.rowDetailViewOptions.onBeforeRowDetailToggle(e, args);
}
return true;
});
this.eventHandler.subscribe(this.onRowBackToViewportRange, (e, args) => {
// when row is back to viewport range, we will re-render the View Component(s)
this.handleOnRowBackToViewportRange(e, args);
if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onRowBackToViewportRange === 'function') {
this.rowDetailViewOptions.onRowBackToViewportRange(e, args);
}
});
this._eventHandler.subscribe(this.onBeforeRowOutOfViewportRange, (event, args) => {
if (typeof this.rowDetailViewOptions?.onBeforeRowOutOfViewportRange === 'function') {
this.rowDetailViewOptions.onBeforeRowOutOfViewportRange(event, args);
}
this.disposeViewByItem(args.item);
});
this.eventHandler.subscribe(this.onRowOutOfViewportRange, (e, args) => {
if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onRowOutOfViewportRange === 'function') {
this.rowDetailViewOptions.onRowOutOfViewportRange(e, args);
}
});
// --
// hook some events needed by the Plugin itself
// we need to redraw the open detail views if we change column position (column reorder)
this.eventHandler.subscribe(this._grid.onColumnsReordered, this.redrawAllViewComponents.bind(this, false));
// on row selection changed, we also need to redraw
if (this.gridOptions.enableRowSelection || this.gridOptions.enableCheckboxSelector) {
this.eventHandler.subscribe(this._grid.onSelectedRowsChanged, this.redrawAllViewComponents.bind(this, false));
}
// on sort, all row detail are collapsed so we can dispose of all the Views as well
this.eventHandler.subscribe(this._grid.onSort, this.disposeAllViewComponents.bind(this));
// redraw all Views whenever certain events are triggered
this._subscriptions.push(this.eventPubSubService?.subscribe(['onFilterChanged', 'onGridMenuColumnsChanged', 'onColumnPickerColumnsChanged'], this.redrawAllViewComponents.bind(this, false)), this.eventPubSubService?.subscribe(['onGridMenuClearAllFilters', 'onGridMenuClearAllSorting'], () => window.setTimeout(() => this.redrawAllViewComponents())));
}
}
return this;
}
/** Redraw (re-render) all the expanded row detail View Components */
redrawAllViewComponents(forceRedraw = false) {
this.resetRenderedRows();
this._views.forEach((view) => {
if (!view.rendered || forceRedraw) {
forceRedraw && view.componentRef?.destroy();
this.redrawViewComponent(view);
}
});
}
/** Redraw the necessary View Component */
redrawViewComponent(createdView) {
const containerElement = this.gridContainerElement.querySelector(`.${ROW_DETAIL_CONTAINER_PREFIX}${createdView.id}`);
if (containerElement) {
this.renderViewModel(createdView.dataContext);
}
}
/** Render (or re-render) the View Component (Row Detail) */
renderPreloadView() {
const containerElement = this.gridContainerElement.querySelector(`.${PRELOAD_CONTAINER_PREFIX}`);
if (this._preloadComponent && containerElement) {
const preloadComp = this.angularUtilService.createAngularComponentAppendToDom(this._preloadComponent, containerElement, {}, { sanitizer: this._grid.sanitizeHtmlString });
this._preloadCompRef = preloadComp.componentRef;
}
}
/** Render (or re-render) the View Component (Row Detail) */
renderViewModel(item) {
const containerElement = this.gridContainerElement.querySelector(`.${ROW_DETAIL_CONTAINER_PREFIX}${item[this.datasetIdPropName]}`);
if (this._viewComponent && containerElement) {
// render row detail
const componentOutput = this.angularUtilService.createAngularComponentAppendToDom(this._viewComponent, containerElement, {
model: item,
addon: this,
grid: this._grid,
dataView: this.dataView,
parent: this.rowDetailViewOptions?.parent,
}, {
sanitizer: this._grid.sanitizeHtmlString,
});
if (componentOutput?.componentRef) {
const viewObj = this._views.find((obj) => obj.id === item[this.datasetIdPropName]);
if (viewObj) {
viewObj.componentRef = componentOutput.componentRef;
viewObj.rendered = true;
}
return viewObj;
}
}
return undefined;
}
// --
// protected functions
// ------------------
disposeViewByItem(item, removeFromArray = false) {
const foundViewIndex = this._views.findIndex((view) => view.id === item[this.datasetIdPropName]);
if (foundViewIndex >= 0) {
this.disposeView(this._views[foundViewIndex]);
if (removeFromArray) {
this._views.splice(foundViewIndex, 1);
}
}
}
disposeView(expandedView) {
expandedView.rendered = false;
const compRef = expandedView?.componentRef;
if (compRef) {
this.appRef.detachView(compRef.hostView);
if (typeof compRef?.destroy === 'function') {
compRef.destroy();
}
return expandedView;
}
}
/**
* notify the onAsyncResponse with the "args.item" (required property)
* the plugin will then use item to populate the row detail panel with the "postTemplate"
* @param item
*/
notifyTemplate(item) {
this.onAsyncResponse.notify({ item, itemDetail: item }, new SlickEventData(), this);
}
/**
* On Processing, we will notify the plugin with the new item detail once backend server call completes
* @param item
*/
async onProcessing(item) {
if (item && typeof this._userProcessFn === 'function') {
let awaitedItemDetail;
const userProcessFn = this._userProcessFn(item);
// wait for the "userProcessFn", once resolved we will save it into the "collection"
const response = await userProcessFn;
if (this.datasetIdPropName in response) {
awaitedItemDetail = response; // from Promise
}
else if ((response && response instanceof Observable) || response instanceof Promise) {
awaitedItemDetail = await castObservableToPromise(this.rxjs, response); // from Angular-http-client
}
if (!awaitedItemDetail || !(this.datasetIdPropName in awaitedItemDetail)) {
throw new Error('[Angular-Slickgrid] could not process the Row Detail, you must make sure that your "process" callback ' +
`returns an item object that has an "${this.datasetIdPropName}" property`);
}
// notify the plugin with the new item details
this.notifyTemplate(awaitedItemDetail || {});
}
}
/**
* Just before the row get expanded or collapsed we will do the following
* First determine if the row is expanding or collapsing,
* if it's expanding we will add it to our View Components reference array if we don't already have it
* or if it's collapsing we will remove it from our View Components reference array
*/
handleOnBeforeRowDetailToggle(_e, args) {
// expanding
if (args?.item?.__collapsed) {
// expanding row detail
const viewInfo = {
id: args.item[this.datasetIdPropName],
dataContext: args.item,
rendered: false,
};
addToArrayWhenNotExists(this._views, viewInfo, this.datasetIdPropName);
}
else {
// collapsing, so dispose of the View/Component
this.disposeViewByItem(args.item, true);
}
}
/** When Row comes back to Viewport Range, we need to redraw the View */
handleOnRowBackToViewportRange(_e, args) {
const viewModel = this._views.find((x) => x.id === args.rowId);
if (viewModel && !viewModel.rendered) {
this.redrawViewComponent(viewModel);
}
}
}
/** Global Grid Options Defaults */
const GlobalGridOptions = {
alwaysShowVerticalScroll: true,
autoEdit: false,
asyncEditorLoading: false,
autoFitColumnsOnFirstLoad: true,
autoResize: {
applyResizeToContainer: true,
autoHeight: true,
autoHeightRecalcRow: 100,
calculateAvailableSizeBy: 'window',
bottomPadding: 20,
minHeight: 250,
minWidth: 300,
rightPadding: 0,
},
cellHighlightCssClass: 'slick-cell-modified',
checkboxSelector: {
cssClass: 'slick-cell-checkboxsel',
},
columnPicker: {
hideForceFitButton: false,
hideSyncResizeButton: true,
headerColumnValueExtractor: pickerHeaderColumnValueExtractor,
},
cellMenu: {
autoAdjustDrop: true,
autoAlignSide: true,
hideCloseButton: true,
hideCommandSection: false,
hideOptionSection: false,
},
contextMenu: {
autoAdjustDrop: true,
autoAlignSide: true,
hideCloseButton: true,
hideClearAllGrouping: false,
hideCollapseAllGroups: false,
hideCommandSection: false,
hideCopyCellValueCommand: false,
hideExpandAllGroups: false,
hideExportCsvCommand: false,
hideExportExcelCommand: false,
hideExportTextDelimitedCommand: true,
hideMenuOnScroll: true,
hideOptionSection: false,
iconCollapseAllGroupsCommand: 'mdi mdi-arrow-collapse',
iconExpandAllGroupsCommand: 'mdi mdi-arrow-expand',
iconClearGroupingCommand: 'mdi mdi-close',
iconCopyCellValueCommand: 'mdi mdi-content-copy',
iconExportCsvCommand: 'mdi mdi-download',
iconExportExcelCommand: 'mdi mdi-file-excel-outline',
iconExportTextDelimitedCommand: 'mdi mdi-download',
showBulletWhenIconMissing: true,
subItemChevronClass: 'mdi mdi-chevron-down mdi-rotate-270',
},
customFooterOptions: {
dateFormat: 'YYYY-MM-DD, hh:mm a',
hideRowSelectionCount: false,
hideTotalItemCount: false,
hideLastUpdateTimestamp: true,
footerHeight: 25,
leftContainerClass: 'col-xs-12 col-sm-5',
rightContainerClass: 'col-xs-6 col-sm-7',
metricSeparator: '|',
metricTexts: {
items: 'items',
itemsKey: 'ITEMS',
itemsSelected: 'items selected',
itemsSelectedKey: 'ITEMS_SELECTED',
of: 'of',
ofKey: 'OF',
},
},
dataView: {
// when enabled, this will preserve the row selection even after filtering/sorting/grouping
syncGridSelection: {
preserveHidden: false,
preserveHiddenOnSelectionChange: true,
},
syncGridSelectionWithBackendService: false, // but disable it when using backend services
},
datasetIdPropertyName: 'id',
defaultFilter: Filters.input,
defaultBackendServiceFilterTypingDebounce: 500,
defaultColumnSortFieldId: 'id',
defaultFilterPlaceholder: '🔎︎',
defaultFilterRangeOperator: OperatorType.rangeInclusive,
editable: false,
enableAutoResize: true,
enableAutoSizeColumns: true,
enableCellNavigation: false,
enableColumnPicker: true,
enableColumnReorder: true,
enableColumnResizeOnDoubleClick: true,
enableContextMenu: true,
enableExcelExport: false,
enableTextExport: false, // CSV/Text with Tab Delimited
enableFilterTrimWhiteSpace: false, // do we want to trim white spaces on all Filters?
enableGridMenu: true,
enableHeaderMenu: true,
enableEmptyDataWarningMessage: true,
emptyDataWarning: {
className: 'slick-empty-data-warning',
message: 'No data to display.',
messageKey: 'EMPTY_DATA_WARNING_MESSAGE',
hideFrozenLeftWarning: false,
hideFrozenRightWarning: false,
leftViewportMarginLeft: '40%',
rightViewportMarginLeft: '40%',
frozenLeftViewportMarginLeft: '0px',
frozenRightViewportMarginLeft: '40%',
},
enableMouseHoverHighlightRow: true,
enableSorting: true,
enableTextSelectionOnCells: true,
eventNamingStyle: EventNamingStyle.camelCase,
explicitInitialization: true,
excelExportOptions: {
addGroupIndentation: true,
exportWithFormatter: false,
filename: 'export',
format: FileType.xlsx,
groupingColumnHeaderTitle: 'Group By',
groupCollapsedSymbol: '⮞',
groupExpandedSymbol: '⮟',
groupingAggregatorRowText: '',
sanitizeDataExport: false,
},
textExportOptions: {
delimiter: DelimiterType.comma,
exportWithFormatter: false,
filename: 'export',
format: FileType.csv,
groupingColumnHeaderTitle: 'Group By',
groupingAggregatorRowText: '',
sanitizeDataExport: false,
useUtf8WithBom: true,
},
filterTypingDebounce: 0,
forceFitColumns: false,
frozenHeaderWidthCalcDifferential: 0,
gridMenu: {
dropSide: 'left',
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',
toggleDarkModeCommandKey: 'TOGGLE_DARK_MODE',
toggleFilterCommandKey: 'TOGGLE_FILTER_ROW',
togglePreHeaderCommandKey: 'TOGGLE_PRE_HEADER_ROW',
},
hideClearAllFiltersCommand: false,
hideClearAllSortingCommand: false,
hideClearFrozenColumnsCommand: true, // opt-in command
hideExportCsvCommand: false,
hideExportExcelCommand: false,
hideExportTextDelimitedCommand: true,
hideForceFitButton: false,
hideRefreshDatasetCommand: false,
hideSyncResizeButton: true,
hideToggleDarkModeCommand: true,
hideToggleFilterCommand: false,
hideTogglePreHeaderCommand: false,
iconCssClass: 'mdi mdi-menu',
iconClearAllFiltersCommand: 'mdi mdi-filter-remove-outline',
iconClearAllSortingCommand: 'mdi mdi-sort-variant-off',
iconClearFrozenColumnsCommand: 'mdi mdi-pin-off-outline',
iconExportCsvCommand: 'mdi mdi-download',
iconExportExcelCommand: 'mdi mdi-file-excel-outline',
iconExportTextDelimitedCommand: 'mdi mdi-download',
iconRefreshDatasetCommand: 'mdi mdi-sync',
iconToggleDarkModeCommand: 'mdi mdi-brightness-4',
iconToggleFilterCommand: 'mdi mdi-flip-vertical',
iconTogglePreHeaderCommand: 'mdi mdi-flip-vertical',
menuWidth: 16,
resizeOnShowHeaderRow: true,
subItemChevronClass: 'mdi mdi-chevron-down mdi-rotate-270',
headerColumnValueExtractor: pickerHeaderColumnValueExtractor,
},
headerMenu: {
autoAlign: true,
autoAlignOffset: 12,
minWidth: 140,
iconClearFilterCommand: 'mdi mdi-filter-remove-outline',
iconClearSortCommand: 'mdi mdi-sort-variant-off',
iconFreezeColumns: 'mdi mdi-pin-outline',
iconSortAscCommand: 'mdi mdi-sort-ascending',
iconSortDescCommand: 'mdi mdi-sort-descending',
iconColumnHideCommand: 'mdi mdi-close',
iconColumnResizeByContentCommand: 'mdi mdi-arrow-expand-horizontal',
hideColumnResizeByContentCommand: false,
hideColumnHideCommand: false,
hideClearFilterCommand: false,
hideClearSortCommand: false,
hideFreezeColumnsCommand: true, // opt-in command
hideSortCommands: false,
subItemChevronClass: 'mdi mdi-chevron-down mdi-rotate-270',
},
headerRowHeight: 35,
multiColumnSort: true,
numberedMultiColumnSort: true,
tristateMultiColumnSort: false,
sortColNumberInSeparateSpan: true,
suppressActiveCellChangeOnEdit: false,
pagination: {
pageSizes: [10, 15, 20, 25, 30, 40, 50, 75, 100],
pageSize: 25,
totalItems: 0,
},
// technically speaking the Row Detail requires the process & viewComponent but we'll ignore it just to set certain options
rowDetailView: {
collapseAllOnSort: true,
cssClass: 'detail-view-toggle',
panelRows: 1,
keyPrefix: '__',
useRowClick: false,
saveDetailViewOnScroll: false,
},
rowHeight: 35,
topPanelHeight: 35,
preHeaderPanelWidth: '100%', // mostly useful for Draggable Grouping dropzone to take full width
translationNamespaceSeparator: ':',
resetFilterSearchValueAfterOnBeforeCancellation: true,
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: '͏͏͏͏͏͏͏͏͏·',
},
};
/**
* Value Extractor for both ColumnPicker & GridMenu Picker
* when using Column Header Grouping, we'll prefix the column group title
* else we'll simply return the column name title
*/
function pickerHeaderColumnValueExtractor(column, gridOptions) {
let colName = column?.columnPickerLabel ?? column?.name ?? '';
if (colName instanceof HTMLElement || colName instanceof DocumentFragment) {
colName = colName.textContent || '';
}
const headerGroup = column?.columnGroup || '';
const columnGroupSeparator = gridOptions?.columnGroupSeparator ?? ' - ';
if (headerGroup) {
return headerGroup + columnGroupSeparator + colName;
}
return colName;
}
class SlickgridConfig {
options;
constructor() {
this.options = GlobalGridOptions;
}
}
class Constants {
// English Locale texts when using only 1 Locale instead of I18N
static locales = {
TEXT_ALL_SELECTED: 'All Selected',
TEXT_ALL_X_RECORDS_SELECTED: 'All {{x}} records selected',
TEXT_APPLY_MASS_UPDATE: 'Apply Mass Update',
TEXT_APPLY_TO_SELECTION: 'Update Selection',
TEXT_CANCEL: 'Cancel',
TEXT_CLEAR_ALL_FILTERS: 'Clear all Filters',
TEXT_CLEAR_ALL_GROUPING: 'Clear all Grouping',
TEXT_CLEAR_ALL_SORTING: 'Clear all Sorting',
TEXT_CLEAR_PINNING: 'Unfreeze Columns/Rows',
TEXT_CLONE: 'Clone',
TEXT_COLLAPSE_ALL_GROUPS: 'Collapse all Groups',
TEXT_CONTAINS: 'Contains',
TEXT_COLUMNS: 'Columns',
TEXT_COLUMN_RESIZE_BY_CONTENT: 'Resize by Content',
TEXT_COMMANDS: 'Commands',
TEXT_COPY: 'Copy',
TEXT_EQUALS: 'Equals',
TEXT_EQUAL_TO: 'Equal to',
TEXT_ENDS_WITH: 'Ends With',
TEXT_ERROR_EDITABLE_GRID_REQUIRED: 'Your grid must be editable in order to use the Composite Editor Modal.',
TEXT_ERROR_ENABLE_CELL_NAVIGATION_REQUIRED: 'Composite Editor requires the flag "enableCellNavigation" to be set to True in your Grid Options.',
TEXT_ERROR_NO_CHANGES_DETECTED: 'Sorry we could not detect any changes.',
TEXT_ERROR_NO_EDITOR_FOUND: 'We could not find any Editor in your Column Definition.',
TEXT_ERROR_NO_RECORD_FOUND: 'No records selected for edit or clone operation.',
TEXT_ERROR_ROW_NOT_EDITABLE: 'Current row is not editable.',
TEXT_ERROR_ROW_SELECTION_REQUIRED: 'You must select some rows before trying to apply new value(s).',
TEXT_EXPAND_ALL_GROUPS: 'Expand all Groups',
TEXT_EXPORT_TO_CSV: 'Export in CSV format',
TEXT_EXPORT_TO_TEXT_FORMAT: 'Export in Text format (Tab delimited)',
TEXT_EXPORT_TO_EXCEL: 'Export to Excel',
TEXT_EXPORT_TO_TAB_DELIMITED: 'Export in Text format (Tab delimited)',
TEXT_FORCE_FIT_COLUMNS: 'Force fit columns',
TEXT_FREEZE_COLUMNS: 'Freeze Columns',
TEXT_GREATER_THAN: 'Greater than',
TEXT_GREATER_THAN_OR_EQUAL_TO: 'Greater than or equal to',
TEXT_GROUP_BY: 'Group By',
TEXT_HIDE_COLUMN: 'Hide Column',
TEXT_ITEMS: 'items',
TEXT_ITEMS_PER_PAGE: 'items per page',
TEXT_ITEMS_SELECTED: 'items selected',
TEXT_OF: 'of',
TEXT_OK: 'OK',
TEXT_LAST_UPDATE: 'Last Update',
TEXT_LESS_THAN: 'Less than',
TEXT_LESS_THAN_OR_EQUAL_TO: 'Less than or equal to',
TEXT_NO_ELEMENTS_FOUND: 'Aucun élément trouvé',
TEXT_NOT_CONTAINS: 'Not contains',
TEXT_NOT_EQUAL_TO: 'Not equal to',
TEXT_PAGE: 'Page',
TEXT_REFRESH_DATASET: 'Refresh Dataset',
TEXT_REMOVE_FILTER: 'Remove Filter',
TEXT_REMOVE_SORT: 'Remove Sort',
TEXT_SAVE: 'Save',
TEXT_SELECT_ALL: 'Select All',
TEXT_SYNCHRONOUS_RESIZE: 'Synchronous resize',
TEXT_SORT_ASCENDING: 'Sort Ascending',
TEXT_SORT_DESCENDING: 'Sort Descending',
TEXT_STARTS_WITH: 'Starts With',
TEXT_TOGGLE_DARK_MODE: 'Toggle Dark Mode',
TEXT_TOGGLE_FILTER_ROW: 'Toggle Filter Row',
TEXT_TOGGLE_PRE_HEADER_ROW: 'Toggle Pre-Header Row',
TEXT_X_OF_Y_SELECTED: '# of % selected',
TEXT_X_OF_Y_MASS_SELECTED: '{{x}} of {{y}} selected',
};
static treeDataProperties = {
CHILDREN_PROP: 'children',
COLLAPSED_PROP: '__collapsed',
HAS_CHILDREN_PROP: '__hasChildren',
TREE_LEVEL_PROP: '__treeLevel',
PARENT_PROP: '__parentId',
};
// some Validation default texts
static VALIDATION_REQUIRED_FIELD = 'Field is required';
static VALIDATION_EDITOR_VALID_NUMBER = 'Please enter a valid number';
static VALIDATION_EDITOR_VALID_INTEGER = 'Please enter a valid integer number';
static VALIDATION_EDITOR_INTEGER_BETWEEN = 'Please enter a valid integer number between {{minValue}} and {{maxValue}}';
static VALIDATION_EDITOR_INTEGER_MAX = 'Please enter a valid integer number that is lower than {{maxValue}}';
static VALIDATION_EDITOR_INTEGER_MAX_INCLUSIVE = 'Please enter a valid integer number that is lower than or equal to {{maxValue}}';
static VALIDATION_EDITOR_INTEGER_MIN = 'Please enter a valid integer number that is greater than {{minValue}}';
static VALIDATION_EDITOR_INTEGER_MIN_INCLUSIVE = 'Please enter a valid integer number that is greater than or equal to {{minValue}}';
static VALIDATION_EDITOR_NUMBER_BETWEEN = 'Please enter a valid number between {{minValue}} and {{maxValue}}';
static VALIDATION_EDITOR_NUMBER_MAX = 'Please enter a valid number that is lower than {{maxValue}}';
static VALIDATION_EDITOR_NUMBER_MAX_INCLUSIVE = 'Please enter a valid number that is lower than or equal to {{maxValue}}';
static VALIDATION_EDITOR_NUMBER_MIN = 'Please enter a valid number that is greater than {{minValue}}';
static VALIDATION_EDITOR_NUMBER_MIN_INCLUSIVE = 'Please enter a valid number that is greater than or equal to {{minValue}}';
static VALIDATION_EDITOR_DECIMAL_BETWEEN = 'Please enter a valid number with a maximum of {{maxDecimal}} decimals';
static VALIDATION_EDITOR_TEXT_LENGTH_BETWEEN = 'Please make sure your text length is between {{minLength}} and {{maxLength}} characters';
static VALIDATION_EDITOR_TEXT_MAX_LENGTH = 'Please make sure your text is less than {{maxLength}} characters';
static VALIDATION_EDITOR_TEXT_MAX_LENGTH_INCLUSIVE = 'Please make sure your text is less than or equal to {{maxLength}} characters';
static VALIDATION_EDITOR_TEXT_MIN_LENGTH = 'Please make sure your text is more than {{minLength}} character(s)';
static VALIDATION_EDITOR_TEXT_MIN_LENGTH_INCLUSIVE = 'Please make sure your text is at least {{minLength}} character(s)';
}
const WARN_NO_PREPARSE_DATE_SIZE = 10000; // data size to warn user when pre-parse isn't enabled
class AngularSlickgridComponent {
angularUtilService;
appRef;
cd;
containerService;
elm;
translate;
translaterService;
forRootConfig;
_dataset;
_columnDefinitions;
_currentDatasetLength = 0;
_darkMode = false;
_eventHandler = new SlickEventHandler();
_eventPubSubService;
_angularGridInstances;
_hideHeaderRowAfterPageLoad = false;
_isAutosizeColsCalled = false;
_isGridInitialized = false;
_isDatasetInitialized = false;
_isDatasetHierarchicalInitialized = false;
_isPaginationInitialized = false;
_isLocalGrid = true;
_paginationOptions;
_registeredResources = [];
_scrollEndCalled = false;
dataView;
slickGrid;
groupingDefinition = {};
groupItemMetadataProvider;
backendServiceApi;
locales;
metrics;
showPagination = false;
serviceList = [];
totalItems = 0;
paginationData;
subscriptions = [];
// components / plugins
slickEmptyWarning;
slickFooter;
slickPagination;
paginationComponent;
slickRowDetailView;
// services
backendUtilityService;
collectionService;
extensionService;
extensionUtility;
filterFactory;
filterService;
gridEventService;
gridService;
gridStateService;
headerGroupingService;
paginationService;
resizerService;
rxjs;
sharedService;
sortService;
treeDataService;
customDataView;
gridId = '';
gridOptions = {};
get paginationOptions() {
return this._paginationOptions;
}
set paginationOptions(newPaginationOptions) {
if (newPaginationOptions && this._paginationOptions) {
this._paginationOptions = { ...this.gridOptions.pagination, ...this._paginationOptions, ...newPaginationOptions };
}
else {
this._paginationOptions = newPaginationOptions;
}
this.gridOptions.pagination = this._paginationOptions ?? this.gridOptions.pagination;
this.paginationService.updateTotalItems(this.gridOptions.pagination?.totalItems ?? 0, true);
}
get columnDefinitions() {
return this._columnDefinitions;
}
set columnDefinitions(columnDefinitions) {
this._columnDefinitions = columnDefinitions;
if (this._isGridInitialized) {
this.updateColumnDefinitionsList(columnDefinitions);
}
if (columnDefinitions.length > 0) {
this.copyColumnWidthsReference(columnDefinitions);
}
}
// make the columnDefinitions a 2-way binding so that plugin adding cols
// are synched on user's side as well (RowMove, RowDetail, RowSelections)
columnDefinitionsChange = new EventEmitter(true);
get dataset() {
return (this.customDataView ? this.slickGrid?.getData?.() : this.dataView?.getItems()) || [];
}
set dataset(newDataset) {
const prevDatasetLn = this._currentDatasetLength;
const isDatasetEqual = dequal(newDataset, this._dataset || []);
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.slickGrid &&
this.gridOptions?.enableTreeData &&
Array.isArray(newDataset) &&
(newDataset.length > 0 || newDataset.length !== prevDatasetLn || !isDatasetEqual)) {
this._isDatasetHierarchicalInitialized = false;
data = this.sortTreeDataset(newDataset, !isDatasetEqual); // if dataset changed, then force a refresh anyway
}
this._dataset = data;
this.refreshGridData(data || []);
this._currentDatasetLength = (newDataset || []).length;
// expand/autofit columns on first page load
// we can assume that if the prevDataset was empty then we are on first load
if (this.slickGrid && this.gridOptions?.autoFitColumnsOnFirstLoad && prevDatasetLn === 0 && !this._isAutosizeColsCalled) {
this.slickGrid.autosizeColumns();
this._isAutosizeColsCalled = true;
}
this.suggestDateParsingWhenHelpful();
}
get datasetHierarchical() {
return this.sharedService.hierarchicalDataset;
}
set datasetHierarchical(newHierarchicalDataset) {
const isDatasetEqual = dequal(newHierarchicalDataset, this.sharedService?.hierarchicalDataset ?? []);
const prevFlatDatasetLn = this._currentDatasetLength;
this.sharedService.hierarchicalDataset = newHierarchicalDataset;
if (newHierarchicalDataset && this.columnDefinitions && this.filterService?.clearFilters) {
this.filterService.clearFilters();
}
// when a hierarchical dataset is set afterward, we can reset the flat dataset and call a tree data sort that will overwrite the flat dataset
if (newHierarchicalDataset && this.slickGrid && this.sortService?.processTreeDataInitialSort) {
this.sortService.processTreeDataInitialSort();
// we also need to reset/refresh the Tree Data filters because if we inserted new item(s) then it might not show up without doing this refresh
// however we need to queue our process until the flat dataset is ready, so we can queue a microtask to execute the DataView refresh only after everything is ready
queueMicrotask(() => {
const flatDatasetLn = this.dataView.getItemCount();
if (flatDatasetLn > 0 && (flatDatasetLn !== prevFlatDatasetLn || !isDatasetEqual)) {
this.filterService.refreshTreeDataFilters();
}
});
this._isDatasetHierarchicalInitialized = true;
}
}
get elementRef() {
return this.elm;
}
get backendService() {
return this.gridOptions?.backendServiceApi?.service;
}
get eventHandler() {
return this._eventHandler;
}
get gridContainerElement() {
return document.querySelector(`#${this.gridOptions.gridContainerId || ''}`);
}
/** GETTER to know if dataset was initialized or not */
get isDatasetInitialized() {
return this._isDatasetInitialized;
}
/** SETTER to change if dataset was initialized or not (stringly used for unit testing purposes) */
set isDatasetInitialized(isInitialized) {
this._isDatasetInitialized = isInitialized;
}
set isDatasetHierarchicalInitialized(isInitialized) {
this._isDatasetHierarchicalInitialized = isInitialized;
}
get registeredResources() {
return this._registeredResources;
}
slickgridHeader;
slickgridFooter;
constructor(angularUtilService, appRef, cd, containerService, elm, translate, translaterService, forRootConfig, externalServices) {
this.angularUtilService = angularUtilService;
this.appRef = appRef;
this.cd = cd;
this.containerService = containerService;
this.elm = elm;
this.translate = translate;
this.translaterService = translaterService;
this.forRootConfig = forRootConfig;
const slickgridConfig = new SlickgridConfig$1();
// initialize and assign all Service Dependencies
this._eventPubSubService = externalServices?.eventPubSubService ?? new EventPubSubService(this.elm.nativeElement);
this._eventPubSubService.eventNamingStyle = EventNamingStyle.camelCase;
this.backendUtilityService = externalServices?.backendUtilityService ?? new BackendUtilityService();
this.gridEventService = externalServices?.gridEventService ?? new GridEventService();
this.sharedService = externalServices?.sharedService ?? new SharedService();
this.collectionService = externalServices?.collectionService ?? new CollectionService(this.translaterService);
// prettier-ignore
this.extensionUtility = externalServices?.extensionUtility ?? new ExtensionUtility(this.sharedService, this.backendUtilityService, this.translaterService);
this.filterFactory = new FilterFactory(slickgridConfig, this.translaterService, this.collectionService);
// prettier-ignore
this.filterService = externalServices?.filterService ?? new FilterService(this.filterFactory, this._eventPubSubService, this.sharedService, this.backendUtilityService);
this.resizerService = externalServices?.resizerService ?? new ResizerService(this._eventPubSubService);
// prettier-ignore
this.sortService = externalServices?.sortService ?? new SortService(this.collectionService, this.sharedService, this._eventPubSubService, this.backendUtilityService);
this.treeDataService =
externalServices?.treeDataService ?? new TreeDataService(this._eventPubSubService, this.sharedService, this.sortService);
// prettier-ignore
this.paginationService = externalServices?.paginationService ?? new PaginationService(this._eventPubSubService, this.sharedService, this.backendUtilityService);
this.extensionS