UNPKG

angular-slickgrid

Version:
979 lines (970 loc) 110 kB
import { GlobalGridOptions as GlobalGridOptions$1, SlickEventHandler, SlickgridConfig as SlickgridConfig$1, BackendUtilityService, GridEventService, SharedService, CollectionService, ExtensionUtility, FilterFactory, FilterService, ResizerService, SortService, TreeDataService, PaginationService, ExtensionService, GridStateService, GridService, HeaderGroupingService, unsubscribeAll, emptyElement, SlickGroupItemMetadataProvider, SlickDataView, autoAddEditorFormatterToColumnsWithEditor, SlickGrid, PluginFlagMappings, isColumnDateType } from '@slickgrid-universal/common'; export * from '@slickgrid-universal/common'; import * as i0 from '@angular/core'; import { ViewContainerRef, Injectable, Inject, Optional, EventEmitter, output, Component, ContentChild, Input, Output } from '@angular/core'; import * as i1 from '@ngx-translate/core'; import { NgTemplateOutlet } from '@angular/common'; import { SlickFooterComponent } from '@slickgrid-universal/custom-footer-component'; import { SlickEmptyWarningComponent } from '@slickgrid-universal/empty-warning-component'; import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; 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'; 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); } // 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 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 = function AngularUtilService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || AngularUtilService)(i0.ɵɵinject(ViewContainerRef)); }; static ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: AngularUtilService, factory: AngularUtilService.ɵfac }); } (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AngularUtilService, [{ type: Injectable }], () => [{ type: i0.ViewContainerRef, decorators: [{ type: Inject, args: [ViewContainerRef] }] }], null); })(); 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 = function ContainerService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ContainerService)(); }; static ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: ContainerService, factory: ContainerService.ɵfac, providedIn: 'root' }); } (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ContainerService, [{ type: Injectable, args: [{ providedIn: 'root', // This ensures it can be injected anywhere }] }], null, null); })(); /** * 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?.getCurrentLang?.() ?? ''; } /** * 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 = function TranslaterService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || TranslaterService)(i0.ɵɵinject(i1.TranslateService, 8)); }; static ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: TranslaterService, factory: TranslaterService.ɵfac }); } (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(TranslaterService, [{ type: Injectable }], () => [{ type: i1.TranslateService, decorators: [{ type: Optional }] }], null); })(); /** * 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(); } } } /** Global Grid Options Defaults */ const GlobalGridOptions = { ...GlobalGridOptions$1, eventNamingStyle: 'camelCase', // 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, }, }; 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_PDF: 'Export to PDF', 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_OPTIONS: 'Options', TEXT_LAST_UPDATE: 'Last Update', TEXT_LESS_THAN: 'Less than', TEXT_LESS_THAN_OR_EQUAL_TO: 'Less than or equal to', TEXT_LOADING: 'Loading...', 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_UNFREEZE_COLUMNS: 'Unfreeze Columns', 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', LAZY_LOADING_PROP: '__lazyLoading', 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 _c0 = ["slickgridHeader"]; const _c1 = ["slickgridFooter"]; function AngularSlickgridComponent_ng_container_3_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelementContainer(0); } } function AngularSlickgridComponent_ng_container_7_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelementContainer(0); } } const WARN_NO_PREPARSE_DATE_SIZE = 10000; // data size to warn user when pre-parse isn't enabled class AngularSlickgridComponent { angularUtilService; appRef; containerService; elm; translate; translaterService; forRootConfig; _dataset; _columns; _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 = ''; options = {}; containerClasses = undefined; get paginationOptions() { return this._paginationOptions; } set paginationOptions(newPaginationOptions) { if (newPaginationOptions && this._paginationOptions) { this._paginationOptions = { ...this.options.pagination, ...this._paginationOptions, ...newPaginationOptions }; } else { this._paginationOptions = newPaginationOptions; } this.options.pagination = this._paginationOptions ?? this.options.pagination; this.paginationService.updateTotalItems(this.options.pagination?.totalItems ?? 0, true); } get columns() { return this._columns; } set columns(columns) { this._columns = columns; if (this._isGridInitialized) { this.updateColumnDefinitionsList(columns); } if (columns.length > 0) { this.copyColumnWidthsReference(columns); } } // make the columnDefinitions a 2-way binding so that plugin adding cols // are synched on user's side as well (RowMove, RowDetail, RowSelections) columnsChange = new EventEmitter(true); // SlickGrid events onActiveCellChanged = output(); onActiveCellPositionChanged = output(); onAddNewRow = output(); onAutosizeColumns = output(); onBeforeAppendCell = output(); onBeforeCellEditorDestroy = output(); onBeforeColumnsResize = output(); onBeforeDestroy = output(); onBeforeEditCell = output(); onBeforeHeaderCellDestroy = output(); onBeforeHeaderRowCellDestroy = output(); onBeforeFooterRowCellDestroy = output(); onBeforeSetColumns = output(); onBeforeSort = output(); onCellChange = output(); onCellCssStylesChanged = output(); onClick = output(); onColumnsDrag = output(); onColumnsReordered = output(); onColumnsResized = output(); onColumnsResizeDblClick = output(); onCompositeEditorChange = output(); onContextMenu = output(); onDrag = output(); onDragEnd = output(); onDragInit = output(); onDragStart = output(); onDragReplaceCells = output(); onDblClick = output(); onFooterContextMenu = output(); onFooterRowCellRendered = output(); onHeaderCellRendered = output(); onFooterClick = output(); onHeaderClick = output(); onHeaderContextMenu = output(); onHeaderMouseEnter = output(); onHeaderMouseLeave = output(); onHeaderRowCellRendered = output(); onHeaderRowMouseEnter = output(); onHeaderRowMouseLeave = output(); onKeyDown = output(); onMouseEnter = output(); onMouseLeave = output(); onValidationError = output(); onViewportChanged = output(); onRendered = output(); onSelectedRowsChanged = output(); onSetOptions = output(); onScroll = output(); onSort = output(); // DataView events onBeforePagingInfoChanged = output(); onGroupExpanded = output(); onGroupCollapsed = output(); onPagingInfoChanged = output(); onRowCountChanged = output(); onRowsChanged = output(); onRowsOrCountChanged = output(); onSelectedRowIdsChanged = output(); onSetItemsCalled = output(); // other Slick Events onAfterMenuShow = output(); onBeforeMenuClose = output(); onBeforeMenuShow = output(); onColumnsChanged = output(); onCommand = output(); onGridMenuColumnsChanged = output(); onMenuClose = output(); onCopyCells = output(); onCopyCancelled = output(); onPasteCells = output(); onBeforePasteCell = output(); // Slickgrid-Universal events onAfterExportToExcel = output(); onBeforeExportToExcel = output(); onBeforeFilterChange = output(); onBeforeFilterClear = output(); onBeforeSearchChange = output(); onBeforeSortChange = output(); onContextMenuClearGrouping = output(); onContextMenuCollapseAllGroups = output(); onContextMenuExpandAllGroups = output(); onOptionSelected = output(); onColumnPickerColumnsChanged = output(); onGridMenuMenuClose = output(); onGridMenuBeforeMenuShow = output(); onGridMenuAfterMenuShow = output(); onGridMenuClearAllPinning = output(); onGridMenuClearAllFilters = output(); onGridMenuClearAllSorting = output(); onGridMenuCommand = output(); onHeaderButtonCommand = output(); onHeaderMenuCommand = output(); onHeaderMenuColumnResizeByContent = output(); onHeaderMenuBeforeMenuShow = output(); onHeaderMenuAfterMenuShow = output(); onHideColumns = output(); onItemsAdded = output(); onItemsDeleted = output(); onItemsUpdated = output(); onItemsUpserted = output(); onFullResizeByContentRequested = output(); onGridStateChanged = output(); onBeforePaginationChange = output(); onPaginationChanged = output(); onPaginationRefreshed = output(); onPaginationVisibilityChanged = output(); onPaginationSetCursorBased = output(); onGridBeforeResize = output(); onGridAfterResize = output(); onBeforeResizeByContent = output(); onAfterResizeByContent = output(); onSortCleared = output(); onFilterChanged = output(); onFilterCleared = output(); onSortChanged = output(); onTreeItemToggled = output(); onTreeFullToggleEnd = output(); onTreeFullToggleStart = output(); // Angular-Slickgrid specific events onBeforeGridCreate = output(); onGridCreated = output(); onDataviewCreated = output(); onAngularGridCreated = output(); onBeforeGridDestroy = output(); onLanguageChange = output(); 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.options?.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.options?.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.columns && 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(); this.treeDataService.initHierarchicalTree(); // 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.options?.backendServiceApi?.service; } get eventHandler() { return this._eventHandler; } get gridContainerElement() { return document.querySelector(`#${this.options.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 = null; slickgridFooter = null; constructor(angularUtilService, appRef, containerService, elm, translate, translaterService, forRootConfig, externalServices) { this.angularUtilService = angularUtilService; this.appRef = appRef; 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 = '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); // prettier-ignore this.treeDataService = externalServices?.treeDataService ?? new TreeDataService(this._eventPubSubService, this.filterService, this.sharedService, this.sortService); // prettier-ignore this.paginationService = externalServices?.paginationService ?? new PaginationService(this._eventPubSubService, this.sharedService, this.backendUtilityService); this.extensionService = externalServices?.extensionService ?? new ExtensionService(this.extensionUtility, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.treeDataService, this.translaterService, () => this.gridService); // prettier-ignore /* v8 ignore next 8 */ this.gridStateService = externalServices?.gridStateService ?? new GridStateService(this.extensionService, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.treeDataService); // prettier-ignore /* v8 ignore next 9 */ this.gridService = externalServices?.gridService ?? new GridService(this.gridStateService, this.filterService, this._eventPubSubService, this.paginationService, this.sharedService, this.sortService, this.treeDataService); this.headerGroupingService = externalServices?.headerGroupingService ?? new HeaderGroupingService(this.extensionUtility); this.serviceList = [ this.containerService, this.extensionService, this.filterService, this.gridEventService, this.gridService, this.gridStateService, this.headerGroupingService, this.paginationService, this.resizerService, this.sortService, this.treeDataService, ]; // register all Service instances in the container this.containerService.registerInstance('ExtensionUtility', this.extensionUtility); this.containerService.registerInstance('FilterService', this.filterService); this.containerService.registerInstance('CollectionService', this.collectionService); this.containerService.registerInstance('ExtensionService', this.extensionService); this.containerService.registerInstance('GridEventService', this.gridEventService); this.containerService.registerInstance('GridService', this.gridService); this.containerService.registerInstance('GridStateService', this.gridStateService); this.containerService.registerInstance('HeaderGroupingService', this.headerGroupingService); this.containerService.registerInstance('PaginationService', this.paginationService); this.containerService.registerInstance('ResizerService', this.resizerService); this.containerService.registerInstance('SharedService', this.sharedService); this.containerService.registerInstance('SortService', this.sortService); this.containerService.registerInstance('EventPubSubService', this._eventPubSubService); this.containerService.registerInstance('PubSubService', this._eventPubSubService); this.containerService.registerInstance('TranslaterService', this.translaterService); this.containerService.registerInstance('TreeDataService', this.treeDataService); } ngAfterViewInit() { if (!this.columns) { throw new Error('Using `<angular-slickgrid>` requires [columns], it seems that you might have forgot to provide the missing bindable input.'); } this.initialization(this._eventHandler); this._isGridInitialized = true; // recheck the empty warning message after grid is shown so that it works in every use case if (this.options?.enableEmptyDataWarningMessage && Array.isArray(this.dataset)) { const finalTotalCount = this.dataset.length; this.displayEmptyDataWarning(finalTotalCount < 1); } // add dark mode CSS class when enabled if (this.options.darkMode) { this.setDarkMode(true); } this.suggestDateParsingWhenHelpful(); } ngOnDestroy() { this._eventPubSubService.publish('onBeforeGridDestroy', this.slickGrid); this.destroy(); } destroy(shouldEmptyDomElementContainer = false) { // dispose of all Services this.serviceList.forEach((service) => { if (typeof service?.dispose === 'function') { service.dispose(); } }); this.serviceList.length = 0; this._eventPubSubService?.unsubscribeAll(); // dispose backend service when defined and a dispose method exists this.backendService?.dispose?.(); // dispose all registered external resources this.disposeExternalResources(); // dispose the Components this.slickEmptyWarning?.dispose(); this.slickFooter?.dispose(); this.slickPagination?.dispose(); if (this._eventHandler?.unsubscribeAll) { this._eventHandler.unsubscribeAll(); } if (this.dataView) { this.dataView.setItems([]); this.dataView.destroy(); } if (this.slickGrid?.destroy) { this.slickGrid.destroy(shouldEmptyDomElementContainer); } if (this.backendServiceApi) { for (const prop of Object.keys(this.backendServiceApi)) { delete this.backendServiceApi[prop]; } this.backendServiceApi = undefined; } if (this.columns) { for (const prop of Object.keys(this.columns)) { this.columns[prop] = null; } } for (const prop of Object.keys(this.sharedService)) { this.sharedService[prop] = null; } // also unsubscribe all RxJS subscriptions this.subscriptions = unsubscribeAll(this.subscriptions); this._dataset = null; this.datasetHierarchical = undefined; this._columns = []; this._angularGridInstances = undefined; this.slickGrid = undefined; // we could optionally also empty the content of the grid container DOM element if (shouldEmptyDomElementContainer) { this.emptyGridContainerElm(); } } disposeExternalResources() { if (Array.isArray(this._registeredResources)) { while (this._registeredResources.length > 0) { const res = this._registeredResources.pop(); if (typeof res?.dispose === 'function') { res.dispose(); } } } this._registeredResources = []; } emptyGridContainerElm() { const gridContainerId = this.options?.gridContainerId || 'grid1'; const gridContainerElm = document.querySelector(`#${gridContainerId}`); emptyElement(gridContainerElm); } /** * Define our internal Post Process callback, it will execute internally after we get back result from the Process backend call * Currently ONLY available with the GraphQL Backend Service. * The behavior is to refresh the Dataset & Pagination without requiring the user to create his own PostProcess every time */ createBackendApiInternalPostProcessCallback(gridOptions) { const backendApi = gridOptions?.backendServiceApi; if (backendApi?.service) { const backendApiService = backendApi.service; // internalPostProcess only works (for now) with a GraphQL Service, so make sure it is of that type if (typeof backendApiService.getDatasetName === 'function') { backendApi.internalPostProcess = (processResult) => { // prettier-ignore const datasetName = backendApi && backendApiService && typeof backendApiService.getDatasetName === 'function' ? backendApiService.getDatasetName() : ''; if (!Array.isArray(processResult) && processResult?.data[datasetName]) { const data = 'nodes' in processResult.data[datasetName] ? processResult.data[datasetName].nodes : processResult.data[datasetName]; const totalCount = 'totalCount' in processResult.data[datasetName] ? processResult.data[datasetName].totalCount : processResult.data[datasetName].length; this.refreshGridData(data, totalCount || 0); } }; } } } initialization(eventHandler) { this.options.translater = this.translaterService; this._eventHandler = eventHandler; this._isAutosizeColsCalled = false; // when detecting a frozen grid, we'll automatically enable the mousewheel scroll handler so that we can scroll from both left/right frozen containers if (this.options && ((this.options.frozenRow !== undefined && this.options.frozenRow >= 0) || (this.options.frozenColumn !== undefined && this.options.frozenColumn >= 0)) && this.options.enableMouseWheelScrollHandler === undefined) { this.options.enableMouseWheelScrollHandler = true; } this._eventPubSubService.eventNamingStyle = this.options?.eventNamingStyle ?? 'camelCase'; this._eventPubSubService.publish('onBeforeGridCreate', true); // make sure the dataset is initialized (if not it will throw an error that it cannot getLength of null) this._dataset ||= []; this.options = this.mergeGridOptions(this.options); this._paginationOptions = this.options?.pagination; this.locales = this.options?.locales ?? Constants.locales; this.backendServiceApi = this.options?.backendServiceApi; this._isLocalGrid = !this.backendServiceApi; // considered a local grid if it doesn't have a backend service set // unless specified, we'll create an internal postProcess callback (currently only available for GraphQL) if (this.options.backendServiceApi && !this.options.backendServiceApi?.disableInternalPostProcess) { this.createBackendApiInternalPostProcessCallback(this.options); } if (!this.customDataView) { const dataviewInlineFilters = this.options?.dataView?.inlineFilters ?? false; let dataViewOptions = { ...this.options.dataView, inlineFilters: dataviewInlineFilters }; if (this.options.draggableGrouping || this.options.enableGrouping) { this.groupItemMetadataProvider = new SlickGroupItemMetadataProvider(this.options.groupItemMetadataOption); this.sharedService.groupItemMetadataProvider = this.groupItemMetadataProvider; dataViewOptions = { ...dataViewOptions, groupItemMetadataProvider: this.groupItemMetadataProvider }; } this.dataView = new SlickDataView(dataViewOptions, this._eventPubSubService); this._eventPubSubService.publish('onDataviewCreated', this.dataView); } // get any possible Services that user want to register which don't require SlickGrid to be instantiated // RxJS Resource is in this lot because it has to be registered before anything else and doesn't require SlickGrid to be initialized this.preRegisterResources(); // prepare and load all SlickGrid editors, if an async editor is found then we'll also execute it. this._columns = this.gridStateService.loadSlickGridEditors(this._columns || []); // if the user wants to automatically add a Custom Editor Formatter, we need to call the auto add function again if (this.options.autoAddCustomEditorFormatter) { autoAddEditorFormatterToColumnsWithEditor(this._columns, this.options.autoAddCustomEditorFormatter); } // save reference for all columns before they optionally become hidden/visible this.sharedService.allColumns = this._columns; // before certain extentions/plugins potentially adds extra columns not created by the user itself (RowMove, RowDetail, RowSelections) // we'll subscribe to the event and push back the change to the user so they always use full column defs array including extra cols this.subscriptions.push(this._eventPubSubService.subscribe('onPluginColumnsChanged', (data) => { this._columns = data.columns; this.columnsChange.emit(this._columns); })); // after subscribing to potential columns changed, we are ready to create these optional extensions // when we did find some to create (RowMove, RowDetail, RowSelections), it will automatically modify column definitions (by previous subscribe) this.extensionService.createExtensionsBeforeGridCreation(this._columns, this.options); // if user entered some Pinning/Frozen "presets", we need to apply them in the grid options if (this.options.presets?.pinning) { this.options = { ...this.options, ...this.options.presets.pinning }; } // build SlickGrid Grid, also user might optionally pass a custom dataview (e.g. remote model) this.slickGrid = new SlickGrid(`#${this.gridId}`, this.customDataView || this.dataView, this._columns, this.options, this._eventPubSubService); if (typeof this.dataView.setGrid === 'function') { this.dataView.setGrid(this.slickGrid); } this.sharedService.dataView = this.dataView; this.sharedService.slickGrid = this.slickGrid; this.sharedService.gridContainerElement = this.elm.nativeElement; if (this.groupItemMetadataProvider) { this.slickGrid.registerPlugin(this.groupItemMetadataProvider); // register GroupItemMetadataProvider when Grouping is enabled } // get any possible Services that user want to register this.registerResources(); this.extensionService.bindDifferentExtensions(); this.bindDifferentHooks(this.slickGrid, this.options, this.dataView); // when it's a frozen grid, we need to keep the frozen column id for reference if we ever show/hide column from ColumnPicker/GridMenu afterward this.sharedService.frozenVisibleColumnId = this.slickGrid.getFrozenColumnId(); // initialize the SlickGrid grid this.slickGrid.init(); // initialized the resizer service only after SlickGrid is initialized // if we don't we end up binding our resize to a grid element that doesn't yet exist in the DOM and the resizer service will fail silently (because it has a try/catch that unbinds the resize without throwing back) if (this.gridContainerElement) { this.resizerService.init(this.slickGrid, this.gridContainerElement); } // user could show a custom footer with the data metrics (dataset length and last updated timestamp) if (!this.options.enablePagination && this.options.showCustomFooter && this.options.customFooterOptions && this.gridContainerElement) { this.slickFooter = new SlickFooterComponent(this.slickGrid, this.options.customFooterOptions, this._eventPubSubService, this.translaterService); this.slickFooter.renderFooter(this.gridContainerElement); } if (!this.customDataView && this.dataView) { // load the data in the DataView (unless it's a hierarchical dataset, if so it will be loaded after the initial tree sort) const initialDataset = this.options?.enableTreeData ? this.sortTreeDataset(this._dataset) : this._dataset; this.dataView.beginUpdate(); this.dataView.setItems(initialDataset || [], this.options.datasetIdPropertyName ?? 'id'); this.dataView.endUpdate(); // if you don't want the items that are not visible (due to being filtered out or being on a different page) // to stay selected, pass 'false' to the second arg if (this.slickGrid?.getSelectionModel() && this.options?.dataView && 'syncGridSelection' in this.options.dataView) { // if we are using a Backend Service, we will do an extra flag check, the reason is because it might have some unintended behaviors // with the BackendServiceApi because technically the data in the page changes the DataView on every page change. let preservedRowSelectionWithBackend = false; if (this.options.backendServiceApi && 'syncGridSelectionWithBackendService' in this.options.dataView) { preservedRowSelectionWithBackend = this.options.dataView.syncGridSelectionWithBackendService; } const syncGridSelection = this.options.dataView.syncGridSelection; if (typeof syncGridSelection === 'boolean') { let preservedRowSelection = syncGridSelection; if (!this._isLocalGrid) { // when using BackendServiceApi, we'll be using the "syncGridSelectionWithBackendService" flag BUT "syncGridSelection" must also be set to True preservedRowSelection = syncGridSelection && preservedRowSelectionWithBackend; } this.dataView.syncGridSelection(this.slickGrid, preservedRowSelection); } else if (typeof syncGridSelection === 'object') { this.dataView.syncGridSelection(this.slickGrid, syncGridSelection.preserveHidden, syncGridSelection.preserveHiddenOnSelectionChange); } } const datasetLn = this.dataView.getLength() || this._dataset?.length || 0; if (datasetLn > 0) { if (!this._isDatasetInitialized && (this.options.enableCheckboxSelector || this.options.enableSelection)) { this.loadRowSelectionPresetWhenExists(); } this.loadFilterPresetsWhenDatasetInitialized(); this._isDatasetInitialized = true; } } // user might want to hide the header row on page load but still have `enableFiltering: true` // if that is the case, we need to hide the headerRow ONLY AFTER all filters got created & dataView exist if (this._hideHeaderRowAfterPageLoad) { this.showHeaderRow(false); this.sharedService.hideHeaderRowAfterPageLoad = this._hideHeaderRowAfterPageLoad; } // publish & dispatch certain events this._eventPubSubService.publish('onGridCreated', this.slickGrid); // after the DataView is created & updated execute some processes if (!this.customDataView) { this.executeAfterDataviewCreated(this.slickGrid, this.options); } // bind resize ONLY after the dataView is ready this.bindResizeHook(this.slickGrid, this.options); // bind the Backend Service API callback functions only after the grid is initialized // because the preProcess() and onInit() might get triggered if (this.options?.backendServiceApi) { this.bindBackendCallbackFunctions(this.options); } // local grid, check if we need to show the Pagination // if so then also check if there's any presets and finally initialize the PaginationService // a local grid with Pagination presets will potentially have a different total of items, we'll need to get it from the DataView and update our total if (this.options?.enablePagination && this._isLocalGrid) { this.showPagination = true; this.loadLocalGridPagination(this.dataset); } this._angularGridInstances = { // Slick Grid & DataView objects dataView: this.dataView, slickGrid: this.slickGrid, extensions: this.extensionService?.extensionList, // public methods destroy: this.destroy.bind(this), // return all available Services (non-singleton) backendService: this.backendService, eventPubSubService: this._eventPubSubService, filterService: this.filterService, gridEventService: this.gridEventService, gridStateService: this.gridStateService, gridService: this.gridService, headerGroupingService: this.headerGroupingService, extensionService: this.extensionService, paginationComponent: this.slickPagination, paginationService: this.paginationService, resizerService: this.resizerService, sortService: this.sortService, treeDataService: this.treeDataService, }; // all instances (SlickGrid, DataView & all Services) this._eventPubSubService.publish('onAngularGridCreated', this._angularGridInstances); } /** * On a Pagination changed, we will trigger a Grid State changed with the new pagination info * Also if we use Row Selection or the Checkbox Selector with a Backend Service (Odata, GraphQL), we need to reset any selection