UNPKG

angular-slickgrid

Version:

Slickgrid components made available in Angular

1,053 lines (1,044 loc) 123 kB
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