UNPKG

slickgrid

Version:

A lightning fast JavaScript grid/spreadsheet

363 lines (361 loc) 23.3 kB
"use strict"; (() => { var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: !0, configurable: !0, writable: !0, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key != "symbol" ? key + "" : key, value); // src/plugins/slick.rowdetailview.ts var SlickEvent = Slick.Event, SlickEventHandler = Slick.EventHandler, Utils = Slick.Utils, SlickRowDetailView = class { /** Constructor of the Row Detail View Plugin which accepts optional options */ constructor(options) { // -- // public API __publicField(this, "pluginName", "RowDetailView"); __publicField(this, "onAsyncResponse", new SlickEvent("onAsyncResponse")); __publicField(this, "onAsyncEndUpdate", new SlickEvent("onAsyncEndUpdate")); __publicField(this, "onAfterRowDetailToggle", new SlickEvent("onAfterRowDetailToggle")); __publicField(this, "onBeforeRowDetailToggle", new SlickEvent("onBeforeRowDetailToggle")); __publicField(this, "onRowBackToViewportRange", new SlickEvent("onRowBackToViewportRange")); __publicField(this, "onRowOutOfViewportRange", new SlickEvent("onRowOutOfViewportRange")); // -- // protected props __publicField(this, "_grid"); __publicField(this, "_gridOptions"); __publicField(this, "_gridUid", ""); __publicField(this, "_dataView"); __publicField(this, "_dataViewIdProperty", "id"); __publicField(this, "_expandableOverride", null); __publicField(this, "_lastRange", null); __publicField(this, "_expandedRows", []); __publicField(this, "_eventHandler"); __publicField(this, "_outsideRange", 5); __publicField(this, "_visibleRenderedCellCount", 0); __publicField(this, "_options"); __publicField(this, "_defaults", { columnId: "_detail_selector", cssClass: "detailView-toggle", expandedClass: void 0, collapsedClass: void 0, keyPrefix: "_", loadOnce: !1, collapseAllOnSort: !0, reorderable: !1, saveDetailViewOnScroll: !0, singleRowExpand: !1, useSimpleViewportCalc: !1, alwaysRenderColumn: !0, toolTip: "", width: 30, maxRows: void 0 }); __publicField(this, "_keyPrefix", this._defaults.keyPrefix); __publicField(this, "_gridRowBuffer", 0); __publicField(this, "_rowIdsOutOfViewport", []); this._options = Utils.extend(!0, {}, this._defaults, options), this._eventHandler = new SlickEventHandler(), typeof this._options.expandableOverride == "function" && this.expandableOverride(this._options.expandableOverride); } /** * Initialize the plugin, which requires user to pass the SlickGrid Grid object * @param grid: SlickGrid Grid object */ init(grid) { var _a, _b; if (!grid) throw new Error('RowDetailView Plugin requires the Grid instance to be passed as argument to the "init()" method'); this._grid = grid, this._gridUid = grid.getUID(), this._gridOptions = grid.getOptions() || {}, this._dataView = this._grid.getData(), this._keyPrefix = (_b = (_a = this._options) == null ? void 0 : _a.keyPrefix) != null ? _b : "_", Utils.addSlickEventPubSubWhenDefined(grid.getPubSubService(), this), this._gridRowBuffer = this._gridOptions.minRowBuffer || 0, this._gridOptions.minRowBuffer = this._options.panelRows + 3, this._eventHandler.subscribe(this._grid.onClick, this.handleClick.bind(this)).subscribe(this._grid.onScroll, this.handleScroll.bind(this)), this._options.collapseAllOnSort && (this._eventHandler.subscribe(this._grid.onSort, this.collapseAll.bind(this)), this._expandedRows = [], this._rowIdsOutOfViewport = []), this._eventHandler.subscribe(this._dataView.onRowCountChanged, () => { this._grid.updateRowCount(), this._grid.render(); }), this._eventHandler.subscribe(this._dataView.onRowsChanged, (_e, a) => { this._grid.invalidateRows(a.rows), this._grid.render(); }), this.subscribeToOnAsyncResponse(), this._eventHandler.subscribe(this._dataView.onSetItemsCalled, () => { var _a2, _b2; this._dataViewIdProperty = (_b2 = (_a2 = this._dataView) == null ? void 0 : _a2.getIdPropertyName()) != null ? _b2 : "id"; }), this._options.useSimpleViewportCalc && this._eventHandler.subscribe(this._grid.onRendered, (_e, args) => { args != null && args.endRow && (this._visibleRenderedCellCount = args.endRow - args.startRow); }); } /** destroy the plugin and it's events */ destroy() { this._eventHandler.unsubscribeAll(), this.onAsyncResponse.unsubscribe(), this.onAsyncEndUpdate.unsubscribe(), this.onAfterRowDetailToggle.unsubscribe(), this.onBeforeRowDetailToggle.unsubscribe(), this.onRowOutOfViewportRange.unsubscribe(), this.onRowBackToViewportRange.unsubscribe(); } /** Get current plugin options */ getOptions() { return this._options; } /** set or change some of the plugin options */ setOptions(options) { var _a; this._options = Utils.extend(!0, {}, this._options, options), (_a = this._options) != null && _a.singleRowExpand && this.collapseAll(); } /** Find a value in an array and return the index when (or -1 when not found) */ arrayFindIndex(sourceArray, value) { if (Array.isArray(sourceArray)) { for (let i = 0; i < sourceArray.length; i++) if (sourceArray[i] === value) return i; } return -1; } /** Handle mouse click event */ handleClick(e, args) { let dataContext = this._grid.getDataItem(args.row); if (this.checkExpandableOverride(args.row, dataContext, this._grid) && (this._options.useRowClick || this._grid.getColumns()[args.cell].id === this._options.columnId && e.target.classList.contains(this._options.cssClass || ""))) { if (this._grid.getEditorLock().isActive() && !this._grid.getEditorLock().commitCurrentEdit()) { e.preventDefault(), e.stopImmediatePropagation(); return; } if (this.onBeforeRowDetailToggle.notify({ grid: this._grid, item: dataContext }, e, this).getReturnValue() === !1) return; this.toggleRowSelection(args.row, dataContext), this.onAfterRowDetailToggle.notify({ grid: this._grid, item: dataContext, expandedRows: this._expandedRows }, e, this), e.stopPropagation(), e.stopImmediatePropagation(); } } /** If we scroll save detail views that go out of cache range */ handleScroll() { this._options.useSimpleViewportCalc ? this.calculateOutOfRangeViewsSimplerVersion() : this.calculateOutOfRangeViews(); } /** Calculate when expanded rows become out of view range */ calculateOutOfRangeViews() { let scrollDir = ""; if (this._grid) { let renderedRange = this._grid.getRenderedRange(); if (this._expandedRows.length > 0 && (scrollDir = "DOWN", this._lastRange)) { if (this._lastRange.top === renderedRange.top && this._lastRange.bottom === renderedRange.bottom) return; (this._lastRange.top > renderedRange.top || // Or we are at very top but our bottom is increasing this._lastRange.top === 0 && renderedRange.top === 0 && this._lastRange.bottom > renderedRange.bottom) && (scrollDir = "UP"); } this._expandedRows.forEach((row) => { var _a, _b; let rowIndex = (_b = (_a = this._dataView) == null ? void 0 : _a.getRowById(row[this._dataViewIdProperty])) != null ? _b : 0, rowPadding = row[`${this._keyPrefix}sizePadding`], rowOutOfRange = this.arrayFindIndex(this._rowIdsOutOfViewport, row[this._dataViewIdProperty]) >= 0; scrollDir === "UP" ? (this._options.saveDetailViewOnScroll && rowIndex >= renderedRange.bottom - this._gridRowBuffer && this.saveDetailView(row), rowOutOfRange && rowIndex - this._outsideRange < renderedRange.top && rowIndex >= renderedRange.top ? this.notifyBackToViewportWhenDomExist(row, row[this._dataViewIdProperty]) : !rowOutOfRange && rowIndex + rowPadding > renderedRange.bottom && this.notifyOutOfViewport(row, row[this._dataViewIdProperty])) : scrollDir === "DOWN" && (this._options.saveDetailViewOnScroll && rowIndex <= renderedRange.top + this._gridRowBuffer && this.saveDetailView(row), rowOutOfRange && rowIndex + rowPadding + this._outsideRange > renderedRange.bottom && rowIndex < rowIndex + rowPadding ? this.notifyBackToViewportWhenDomExist(row, row[this._dataViewIdProperty]) : !rowOutOfRange && rowIndex < renderedRange.top && this.notifyOutOfViewport(row, row[this._dataViewIdProperty])); }), this._lastRange = renderedRange; } } /** This is an alternative & more simpler version of the Calculate when expanded rows become out of view range */ calculateOutOfRangeViewsSimplerVersion() { if (this._grid) { let renderedRange = this._grid.getRenderedRange(); this._expandedRows.forEach((row) => { var _a; let rowIndex = (_a = this._dataView.getRowById(row[this._dataViewIdProperty])) != null ? _a : -1, isOutOfVisibility = this.checkIsRowOutOfViewportRange(rowIndex, renderedRange); !isOutOfVisibility && this.arrayFindIndex(this._rowIdsOutOfViewport, row[this._dataViewIdProperty]) >= 0 ? this.notifyBackToViewportWhenDomExist(row, row[this._dataViewIdProperty]) : isOutOfVisibility && this.notifyOutOfViewport(row, row[this._dataViewIdProperty]); }); } } /** * Check if the row became out of visible range (when user can't see it anymore) * @param rowIndex * @param renderedRange from SlickGrid */ checkIsRowOutOfViewportRange(rowIndex, renderedRange) { return Math.abs(renderedRange.bottom - this._gridRowBuffer - rowIndex) > this._visibleRenderedCellCount * 2; } /** Send a notification, through "onRowOutOfViewportRange", that is out of the viewport range */ notifyOutOfViewport(item, rowId) { let rowIndex = item.rowIndex || this._dataView.getRowById(item[this._dataViewIdProperty]); this.onRowOutOfViewportRange.notify({ grid: this._grid, item, rowId, rowIndex, expandedRows: this._expandedRows, rowIdsOutOfViewport: this.syncOutOfViewportArray(rowId, !0) }, null, this); } /** Send a notification, through "onRowBackToViewportRange", that a row came back into the viewport visible range */ notifyBackToViewportWhenDomExist(item, rowId) { let rowIndex = item.rowIndex || this._dataView.getRowById(item[this._dataViewIdProperty]); window.setTimeout(() => { document.querySelector(`.${this._gridUid} .cellDetailView_${item[this._dataViewIdProperty]}`) && this.onRowBackToViewportRange.notify({ grid: this._grid, item, rowId, rowIndex, expandedRows: this._expandedRows, rowIdsOutOfViewport: this.syncOutOfViewportArray(rowId, !1) }, null, this); }, 100); } /** * This function will sync the "out of viewport" array whenever necessary. * The sync can add a detail row (when necessary, no need to add again if it already exist) or delete a row from the array. * @param rowId: number * @param isAdding: are we adding or removing a row? */ syncOutOfViewportArray(rowId, isAdding) { let arrayRowIndex = this.arrayFindIndex(this._rowIdsOutOfViewport, rowId); return isAdding && arrayRowIndex < 0 ? this._rowIdsOutOfViewport.push(rowId) : !isAdding && arrayRowIndex >= 0 && this._rowIdsOutOfViewport.splice(arrayRowIndex, 1), this._rowIdsOutOfViewport; } // Toggle between showing or hiding a row toggleRowSelection(rowNumber, dataContext) { this.checkExpandableOverride(rowNumber, dataContext, this._grid) && (this._dataView.beginUpdate(), this.handleAccordionShowHide(dataContext), this._dataView.endUpdate()); } /** Collapse all of the open detail rows */ collapseAll() { this._dataView.beginUpdate(); for (let i = this._expandedRows.length - 1; i >= 0; i--) this.collapseDetailView(this._expandedRows[i], !0); this._dataView.endUpdate(); } /** Collapse a detail row so that it is not longer open */ collapseDetailView(item, isMultipleCollapsing = !1) { isMultipleCollapsing || this._dataView.beginUpdate(), this._options.loadOnce && this.saveDetailView(item), item[`${this._keyPrefix}collapsed`] = !0; for (let idx = 1; idx <= item[`${this._keyPrefix}sizePadding`]; idx++) this._dataView.deleteItem(item[this._dataViewIdProperty] + "." + idx); item[`${this._keyPrefix}sizePadding`] = 0, this._dataView.updateItem(item[this._dataViewIdProperty], item), this._expandedRows = this._expandedRows.filter((r) => r[this._dataViewIdProperty] !== item[this._dataViewIdProperty]), isMultipleCollapsing || this._dataView.endUpdate(); } /** Expand a detail row by providing the dataview item that is to be expanded */ expandDetailView(item) { var _a, _b, _c; if ((_a = this._options) != null && _a.singleRowExpand && this.collapseAll(), item[`${this._keyPrefix}collapsed`] = !1, this._expandedRows.push(item), item[`${this._keyPrefix}detailContent`] || (item[`${this._keyPrefix}detailViewLoaded`] = !1), !item[`${this._keyPrefix}detailViewLoaded`] || this._options.loadOnce !== !0) item[`${this._keyPrefix}detailContent`] = (_c = (_b = this._options) == null ? void 0 : _b.preTemplate) == null ? void 0 : _c.call(_b, item); else { this.onAsyncResponse.notify({ item, itemDetail: item, detailView: item[`${this._keyPrefix}detailContent`], grid: this._grid }, void 0, this), this.applyTemplateNewLineHeight(item), this._dataView.updateItem(item[this._dataViewIdProperty], item); return; } this.applyTemplateNewLineHeight(item), this._dataView.updateItem(item[this._dataViewIdProperty], item), this._options.process(item); } /** Saves the current state of the detail view */ saveDetailView(item) { let view = document.querySelector(`.${this._gridUid} .innerDetailView_${item[this._dataViewIdProperty]}`); if (view) { let html = view.innerHTML; html !== void 0 && (item[`${this._keyPrefix}detailContent`] = html); } } /** * subscribe to the onAsyncResponse so that the plugin knows when the user server side calls finished * the response has to be as "args.item" (or "args.itemDetail") with it's data back */ subscribeToOnAsyncResponse() { this.onAsyncResponse.subscribe((e, args) => { var _a, _b; if (!args || !args.item && !args.itemDetail) throw 'Slick.RowDetailView plugin requires the onAsyncResponse() to supply "args.item" property.'; let itemDetail = args.item || args.itemDetail; args.detailView ? itemDetail[`${this._keyPrefix}detailContent`] = args.detailView : itemDetail[`${this._keyPrefix}detailContent`] = (_b = (_a = this._options) == null ? void 0 : _a.postTemplate) == null ? void 0 : _b.call(_a, itemDetail), itemDetail[`${this._keyPrefix}detailViewLoaded`] = !0, this._dataView.updateItem(itemDetail[this._dataViewIdProperty], itemDetail), this.onAsyncEndUpdate.notify({ grid: this._grid, item: itemDetail, itemDetail }, e, this); }); } /** When row is getting toggled, we will handle the action of collapsing/expanding */ handleAccordionShowHide(item) { item && (item[`${this._keyPrefix}collapsed`] ? this.expandDetailView(item) : this.collapseDetailView(item)); } ////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////// /** Get the Row Detail padding (which are the rows dedicated to the detail panel) */ getPaddingItem(parent, offset) { let item = {}; return Object.keys(this._dataView).forEach((prop) => { item[prop] = null; }), item[this._dataViewIdProperty] = parent[this._dataViewIdProperty] + "." + offset, item[`${this._keyPrefix}collapsed`] = !0, item[`${this._keyPrefix}isPadding`] = !0, item[`${this._keyPrefix}parent`] = parent, item[`${this._keyPrefix}offset`] = offset, item; } /** Create the detail ctr node. this belongs to the dev & can be custom-styled as per */ applyTemplateNewLineHeight(item) { var _a; let rowCount = this._options.panelRows, lineHeight = 13; item[`${this._keyPrefix}sizePadding`] = Math.ceil(rowCount * 2 * lineHeight / this._gridOptions.rowHeight), item[`${this._keyPrefix}height`] = item[`${this._keyPrefix}sizePadding`] * this._gridOptions.rowHeight; let idxParent = (_a = this._dataView.getIdxById(item[this._dataViewIdProperty])) != null ? _a : 0; for (let idx = 1; idx <= item[`${this._keyPrefix}sizePadding`]; idx++) this._dataView.insertItem(idxParent + idx, this.getPaddingItem(item, idx)); } /** Get the Column Definition of the first column dedicated to toggling the Row Detail View */ getColumnDefinition() { return { id: this._options.columnId, name: "", reorderable: this._options.reorderable, toolTip: this._options.toolTip, field: "sel", width: this._options.width, resizable: !1, sortable: !1, alwaysRenderColumn: this._options.alwaysRenderColumn, cssClass: this._options.cssClass, formatter: this.detailSelectionFormatter.bind(this) }; } /** Return the currently expanded rows */ getExpandedRows() { return this._expandedRows; } /** The cell Formatter that shows the icon that will be used to toggle the Row Detail */ detailSelectionFormatter(row, _cell, _val, _column, dataContext, grid) { if (this.checkExpandableOverride(row, dataContext, grid)) { if (dataContext[`${this._keyPrefix}collapsed`] === void 0 && (dataContext[`${this._keyPrefix}collapsed`] = !0, dataContext[`${this._keyPrefix}sizePadding`] = 0, dataContext[`${this._keyPrefix}height`] = 0, dataContext[`${this._keyPrefix}isPadding`] = !1, dataContext[`${this._keyPrefix}parent`] = void 0, dataContext[`${this._keyPrefix}offset`] = 0), !dataContext[`${this._keyPrefix}isPadding`]) if (dataContext[`${this._keyPrefix}collapsed`]) { let collapsedClasses = this._options.cssClass + " expand "; return this._options.collapsedClass && (collapsedClasses += this._options.collapsedClass), Utils.createDomElement("div", { className: collapsedClasses }); } else { let rowHeight = this._gridOptions.rowHeight, outterHeight = dataContext[`${this._keyPrefix}sizePadding`] * this._gridOptions.rowHeight; this._options.maxRows !== void 0 && dataContext[`${this._keyPrefix}sizePadding`] > this._options.maxRows && (outterHeight = this._options.maxRows * rowHeight, dataContext[`${this._keyPrefix}sizePadding`] = this._options.maxRows); let expandedClasses = this._options.cssClass + " collapse "; this._options.expandedClass && (expandedClasses += this._options.expandedClass); let cellDetailContainerElm = Utils.createDomElement("div", { className: `dynamic-cell-detail cellDetailView_${dataContext[this._dataViewIdProperty]}`, style: { height: `${outterHeight}px`, top: `${rowHeight}px` } }), innerContainerElm = Utils.createDomElement("div", { className: `detail-container detailViewContainer_${dataContext[this._dataViewIdProperty]}` }), innerDetailViewElm = Utils.createDomElement("div", { className: `innerDetailView_${dataContext[this._dataViewIdProperty]}` }); return innerDetailViewElm.innerHTML = this._grid.sanitizeHtmlString(dataContext[`${this._keyPrefix}detailContent`]), innerContainerElm.appendChild(innerDetailViewElm), cellDetailContainerElm.appendChild(innerContainerElm), { html: Utils.createDomElement("div", { className: expandedClasses }), insertElementAfterTarget: cellDetailContainerElm }; } } else return ""; return ""; } /** Resize the Row Detail View */ resizeDetailView(item) { var _a; if (!item) return; let mainContainer = document.querySelector(`.${this._gridUid} .detailViewContainer_${item[this._dataViewIdProperty]}`), cellItem = document.querySelector(`.${this._gridUid} .cellDetailView_${item[this._dataViewIdProperty]}`), inner = document.querySelector(`.${this._gridUid} .innerDetailView_${item[this._dataViewIdProperty]}`); if (!mainContainer || !cellItem || !inner) return; for (let idx = 1; idx <= item[`${this._keyPrefix}sizePadding`]; idx++) this._dataView.deleteItem(`${item[this._dataViewIdProperty]}.${idx}`); let rowHeight = this._gridOptions.rowHeight, lineHeight = 13; mainContainer.style.minHeight = ""; let itemHeight = mainContainer.scrollHeight, rowCount = Math.ceil(itemHeight / rowHeight); item[`${this._keyPrefix}sizePadding`] = Math.ceil(rowCount * 2 * lineHeight / rowHeight), item[`${this._keyPrefix}height`] = itemHeight; let outterHeight = item[`${this._keyPrefix}sizePadding`] * rowHeight; this._options.maxRows !== void 0 && item[`${this._keyPrefix}sizePadding`] > this._options.maxRows && (outterHeight = this._options.maxRows * rowHeight, item[`${this._keyPrefix}sizePadding`] = this._options.maxRows), this._grid.getOptions().minRowBuffer < item[`${this._keyPrefix}sizePadding`] && (this._grid.getOptions().minRowBuffer = item[`${this._keyPrefix}sizePadding`] + 3), mainContainer.setAttribute("style", "min-height: " + item[`${this._keyPrefix}height`] + "px"), cellItem && cellItem.setAttribute("style", "height: " + outterHeight + "px; top:" + rowHeight + "px"); let idxParent = (_a = this._dataView.getIdxById(item[this._dataViewIdProperty])) != null ? _a : 0; for (let idx = 1; idx <= item[`${this._keyPrefix}sizePadding`]; idx++) this._dataView.insertItem(idxParent + idx, this.getPaddingItem(item, idx)); this.saveDetailView(item); } /** Takes in the item we are filtering and if it is an expanded row returns it's parents row to filter on */ getFilterItem(item) { return item[`${this._keyPrefix}isPadding`] && item[`${this._keyPrefix}parent`] && (item = item[`${this._keyPrefix}parent`]), item; } checkExpandableOverride(row, dataContext, grid) { return typeof this._expandableOverride == "function" ? this._expandableOverride(row, dataContext, grid) : !0; } /** * Method that user can pass to override the default behavior or making every row an expandable row. * In order word, user can choose which rows to be an available row detail (or not) by providing his own logic. * @param overrideFn: override function callback */ expandableOverride(overrideFn) { this._expandableOverride = overrideFn; } }; window.Slick && Utils.extend(!0, window, { Slick: { Plugins: { RowDetailView: SlickRowDetailView } } }); })(); //# sourceMappingURL=slick.rowdetailview.js.map