@slickgrid-universal/row-detail-view-plugin 5.0.0-beta.0 → 5.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/slickRowDetailView.ts +782 -782
|
@@ -1,782 +1,782 @@
|
|
|
1
|
-
import { createDomElement, SlickEvent, SlickEventHandler, Utils as SlickUtils } from '@slickgrid-universal/common';
|
|
2
|
-
import type {
|
|
3
|
-
Column,
|
|
4
|
-
ExternalResource,
|
|
5
|
-
FormatterResultWithHtml,
|
|
6
|
-
GridOption,
|
|
7
|
-
OnAfterRowDetailToggleArgs,
|
|
8
|
-
OnBeforeRowDetailToggleArgs,
|
|
9
|
-
OnRowBackToViewportRangeArgs,
|
|
10
|
-
OnRowDetailAsyncEndUpdateArgs,
|
|
11
|
-
OnRowDetailAsyncResponseArgs,
|
|
12
|
-
OnRowOutOfViewportRangeArgs,
|
|
13
|
-
PubSubService,
|
|
14
|
-
RowDetailView,
|
|
15
|
-
RowDetailViewOption,
|
|
16
|
-
SlickGrid,
|
|
17
|
-
SlickRowDetailView as UniversalRowDetailView,
|
|
18
|
-
SlickDataView,
|
|
19
|
-
SlickEventData,
|
|
20
|
-
UsabilityOverrideFn,
|
|
21
|
-
} from '@slickgrid-universal/common';
|
|
22
|
-
import { classNameToList, extend } from '@slickgrid-universal/utils';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* A plugin to add Row Detail Panel View (for example providing order detail info when clicking on the order row in the grid)
|
|
26
|
-
* Original StackOverflow question & article making this possible (thanks to violet313)
|
|
27
|
-
* https://stackoverflow.com/questions/10535164/can-slickgrids-row-height-be-dynamically-altered#29399927
|
|
28
|
-
* http://violet313.org/slickgrids/#intro
|
|
29
|
-
*/
|
|
30
|
-
export class SlickRowDetailView implements ExternalResource, UniversalRowDetailView {
|
|
31
|
-
// --
|
|
32
|
-
// public API
|
|
33
|
-
pluginName = 'RowDetailView' as const;
|
|
34
|
-
|
|
35
|
-
/** Fired when the async response finished */
|
|
36
|
-
onAsyncEndUpdate = new SlickEvent<OnRowDetailAsyncEndUpdateArgs>('onAsyncEndUpdate');
|
|
37
|
-
|
|
38
|
-
/** This event must be used with the "notify" by the end user once the Asynchronous Server call returns the item detail */
|
|
39
|
-
onAsyncResponse = new SlickEvent<OnRowDetailAsyncResponseArgs>('onAsyncResponse');
|
|
40
|
-
|
|
41
|
-
/** Fired after the row detail gets toggled */
|
|
42
|
-
onAfterRowDetailToggle = new SlickEvent<OnAfterRowDetailToggleArgs>('onAfterRowDetailToggle');
|
|
43
|
-
|
|
44
|
-
/** Fired before the row detail gets toggled */
|
|
45
|
-
onBeforeRowDetailToggle = new SlickEvent<OnBeforeRowDetailToggleArgs>('onBeforeRowDetailToggle');
|
|
46
|
-
|
|
47
|
-
/** Fired after the row detail gets toggled */
|
|
48
|
-
onRowBackToViewportRange = new SlickEvent<OnRowBackToViewportRangeArgs>('onRowBackToViewportRange');
|
|
49
|
-
|
|
50
|
-
/** Fired after a row becomes out of viewport range (when user can't see the row anymore) */
|
|
51
|
-
onRowOutOfViewportRange = new SlickEvent<OnRowOutOfViewportRangeArgs>('onRowOutOfViewportRange');
|
|
52
|
-
|
|
53
|
-
// --
|
|
54
|
-
// protected props
|
|
55
|
-
protected _addonOptions!: RowDetailView;
|
|
56
|
-
protected _dataViewIdProperty = 'id';
|
|
57
|
-
protected _eventHandler: SlickEventHandler;
|
|
58
|
-
protected _expandableOverride: UsabilityOverrideFn | null = null;
|
|
59
|
-
protected _expandedRows: any[] = [];
|
|
60
|
-
protected _grid!: SlickGrid;
|
|
61
|
-
protected _gridRowBuffer = 0;
|
|
62
|
-
protected _gridUid = '';
|
|
63
|
-
protected _keyPrefix = '';
|
|
64
|
-
protected _lastRange: { bottom: number; top: number; } | null = null;
|
|
65
|
-
protected _outsideRange = 5;
|
|
66
|
-
protected _rowIdsOutOfViewport: Array<number | string> = [];
|
|
67
|
-
protected _visibleRenderedCellCount = 0;
|
|
68
|
-
protected _defaults = {
|
|
69
|
-
alwaysRenderColumn: true,
|
|
70
|
-
columnId: '_detail_selector',
|
|
71
|
-
field: '_detail_selector',
|
|
72
|
-
cssClass: 'detailView-toggle',
|
|
73
|
-
collapseAllOnSort: true,
|
|
74
|
-
collapsedClass: undefined,
|
|
75
|
-
expandedClass: undefined,
|
|
76
|
-
keyPrefix: '_',
|
|
77
|
-
loadOnce: false,
|
|
78
|
-
maxRows: undefined,
|
|
79
|
-
reorderable: false,
|
|
80
|
-
saveDetailViewOnScroll: true,
|
|
81
|
-
singleRowExpand: false,
|
|
82
|
-
useSimpleViewportCalc: false,
|
|
83
|
-
toolTip: '',
|
|
84
|
-
width: 30,
|
|
85
|
-
} as unknown as RowDetailView;
|
|
86
|
-
|
|
87
|
-
/** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */
|
|
88
|
-
constructor(protected readonly pubSubService: PubSubService) {
|
|
89
|
-
this._eventHandler = new SlickEventHandler();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
get addonOptions() {
|
|
93
|
-
return this._addonOptions;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Getter of SlickGrid DataView object */
|
|
97
|
-
get dataView(): SlickDataView {
|
|
98
|
-
return this._grid?.getData<SlickDataView>();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
get dataViewIdProperty(): string {
|
|
102
|
-
return this._dataViewIdProperty;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
get eventHandler() {
|
|
106
|
-
return this._eventHandler;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Getter for the Grid Options pulled through the Grid Object */
|
|
110
|
-
get gridOptions(): GridOption {
|
|
111
|
-
return this._grid?.getOptions() || {};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
get gridUid() {
|
|
115
|
-
return this._gridUid || (this._grid?.getUID() || '');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
set lastRange(range: { bottom: number; top: number; }) {
|
|
119
|
-
this._lastRange = range;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
set rowIdsOutOfViewport(rowIds: Array<string | number>) {
|
|
123
|
-
this._rowIdsOutOfViewport = rowIds;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
get visibleRenderedCellCount(): number {
|
|
127
|
-
return this._visibleRenderedCellCount;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Initialize the Export Service
|
|
132
|
-
* @param _grid
|
|
133
|
-
* @param _containerService
|
|
134
|
-
*/
|
|
135
|
-
init(grid: SlickGrid) {
|
|
136
|
-
this._grid = grid;
|
|
137
|
-
if (!grid) {
|
|
138
|
-
throw new Error('[Slickgrid-Universal] RowDetailView Plugin requires the Grid instance to be passed as argument to the "init()" method.');
|
|
139
|
-
}
|
|
140
|
-
this._grid = grid;
|
|
141
|
-
this._gridUid = grid.getUID();
|
|
142
|
-
if (!this._addonOptions) {
|
|
143
|
-
this._addonOptions = extend(true, {}, this._defaults, this.gridOptions.rowDetailView) as RowDetailView;
|
|
144
|
-
}
|
|
145
|
-
this._keyPrefix = this._addonOptions?.keyPrefix || '_';
|
|
146
|
-
|
|
147
|
-
// add PubSub instance to all SlickEvent
|
|
148
|
-
SlickUtils.addSlickEventPubSubWhenDefined(this.pubSubService, this);
|
|
149
|
-
|
|
150
|
-
// Update the minRowBuffer so that the view doesn't disappear when it's at top of screen + the original default 3
|
|
151
|
-
this._gridRowBuffer = this.gridOptions.minRowBuffer || 0;
|
|
152
|
-
this.gridOptions.minRowBuffer = this._addonOptions.panelRows + 3;
|
|
153
|
-
|
|
154
|
-
this._eventHandler
|
|
155
|
-
.subscribe(this._grid.onClick, this.handleClick.bind(this))
|
|
156
|
-
.subscribe(this._grid.onBeforeEditCell, () => this.collapseAll())
|
|
157
|
-
.subscribe(this._grid.onScroll, this.handleScroll.bind(this));
|
|
158
|
-
|
|
159
|
-
// Sort will, by default, Collapse all of the open items (unless user implements his own onSort which deals with open row and padding)
|
|
160
|
-
if (this._addonOptions.collapseAllOnSort) {
|
|
161
|
-
// sort event can be triggered by column header click or from header menu
|
|
162
|
-
this.pubSubService.subscribe('onSortChanged', () => this.collapseAll());
|
|
163
|
-
this._expandedRows = [];
|
|
164
|
-
this._rowIdsOutOfViewport = [];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
this._eventHandler.subscribe(this.dataView.onRowCountChanged, () => {
|
|
168
|
-
this._grid.updateRowCount();
|
|
169
|
-
this._grid.render();
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
this._eventHandler.subscribe(this.dataView.onRowsChanged, (_e: SlickEventData, args: { rows: number[]; }) => {
|
|
173
|
-
this._grid.invalidateRows(args.rows);
|
|
174
|
-
this._grid.render();
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// subscribe to the onAsyncResponse so that the plugin knows when the user server side calls finished
|
|
178
|
-
this._eventHandler.subscribe(this.onAsyncResponse, this.handleOnAsyncResponse.bind(this));
|
|
179
|
-
|
|
180
|
-
// after data is set, let's get the DataView Id Property name used (defaults to "id")
|
|
181
|
-
this._eventHandler.subscribe(this.dataView.onSetItemsCalled, () => {
|
|
182
|
-
this._dataViewIdProperty = this.dataView?.getIdPropertyName() || 'id';
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// if we use the alternative & simpler calculation of the out of viewport range
|
|
186
|
-
// we will need to know how many rows are rendered on the screen and we need to wait for grid to be rendered
|
|
187
|
-
// unfortunately there is no triggered event for knowing when grid is finished, so we use 250ms delay and it's typically more than enough
|
|
188
|
-
if (this._addonOptions.useSimpleViewportCalc) {
|
|
189
|
-
this._eventHandler.subscribe(this._grid.onRendered, (_e: SlickEventData, args: { endRow: number; startRow: number; }) => {
|
|
190
|
-
if (args?.endRow) {
|
|
191
|
-
this._visibleRenderedCellCount = args.endRow - args.startRow;
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Dispose of the Slick Row Detail View */
|
|
198
|
-
dispose() {
|
|
199
|
-
this._eventHandler?.unsubscribeAll();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
create(columnDefinitions: Column[], gridOptions: GridOption): UniversalRowDetailView | null {
|
|
203
|
-
if (!gridOptions.rowDetailView) {
|
|
204
|
-
throw new Error('[Slickgrid-Universal] The Row Detail View requires options to be passed via the "rowDetailView" property of the Grid Options');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
this._addonOptions = extend(true, {}, this._defaults, gridOptions.rowDetailView) as RowDetailView;
|
|
208
|
-
|
|
209
|
-
// user could override the expandable icon logic from within the options or after instantiating the plugin
|
|
210
|
-
if (typeof this._addonOptions.expandableOverride === 'function') {
|
|
211
|
-
this.expandableOverride(this._addonOptions.expandableOverride);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (Array.isArray(columnDefinitions) && gridOptions) {
|
|
215
|
-
const newRowDetailViewColumn: Column = this.getColumnDefinition();
|
|
216
|
-
|
|
217
|
-
// add new row detail column unless it was already added
|
|
218
|
-
if (!columnDefinitions.some(col => col.id === newRowDetailViewColumn.id)) {
|
|
219
|
-
const rowDetailColDef = Array.isArray(columnDefinitions) && columnDefinitions.find(col => col?.behavior === 'selectAndMove');
|
|
220
|
-
const finalRowDetailViewColumn = rowDetailColDef ? rowDetailColDef : newRowDetailViewColumn;
|
|
221
|
-
|
|
222
|
-
// column index position in the grid
|
|
223
|
-
const columnPosition = gridOptions?.rowDetailView?.columnIndexPosition ?? 0;
|
|
224
|
-
if (columnPosition > 0) {
|
|
225
|
-
columnDefinitions.splice(columnPosition, 0, finalRowDetailViewColumn);
|
|
226
|
-
} else {
|
|
227
|
-
columnDefinitions.unshift(finalRowDetailViewColumn);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
this.pubSubService.publish(`onPluginColumnsChanged`, {
|
|
231
|
-
columns: columnDefinitions,
|
|
232
|
-
pluginName: this.pluginName
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return this as unknown as UniversalRowDetailView;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** Get current plugin options */
|
|
240
|
-
getOptions(): RowDetailViewOption {
|
|
241
|
-
return this._addonOptions;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** set or change some of the plugin options */
|
|
245
|
-
setOptions(options: Partial<RowDetailViewOption>) {
|
|
246
|
-
this._addonOptions = extend(true, {}, this._addonOptions, options) as RowDetailView;
|
|
247
|
-
if (this._addonOptions?.singleRowExpand) {
|
|
248
|
-
this.collapseAll();
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** Collapse all of the open items */
|
|
253
|
-
collapseAll() {
|
|
254
|
-
this.dataView.beginUpdate();
|
|
255
|
-
this._expandedRows.forEach(expandedRow => {
|
|
256
|
-
this.collapseDetailView(expandedRow, true);
|
|
257
|
-
});
|
|
258
|
-
this.dataView.endUpdate();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** Colapse an Item so it is not longer seen */
|
|
262
|
-
collapseDetailView(item: any, isMultipleCollapsing = false) {
|
|
263
|
-
if (!isMultipleCollapsing) {
|
|
264
|
-
this.dataView.beginUpdate();
|
|
265
|
-
}
|
|
266
|
-
// Save the details on the collapse assuming onetime loading
|
|
267
|
-
if (this._addonOptions.loadOnce) {
|
|
268
|
-
this.saveDetailView(item);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
item[`${this._keyPrefix}collapsed`] = true;
|
|
272
|
-
for (let idx = 1; idx <= item[`${this._keyPrefix}sizePadding`]; idx++) {
|
|
273
|
-
this.dataView.deleteItem(`${item[this._dataViewIdProperty]}.${idx}`);
|
|
274
|
-
}
|
|
275
|
-
item[`${this._keyPrefix}sizePadding`] = 0;
|
|
276
|
-
this.dataView.updateItem(item[this._dataViewIdProperty], item);
|
|
277
|
-
|
|
278
|
-
// Remove the item from the expandedRows
|
|
279
|
-
this._expandedRows = this._expandedRows.filter((expRow) => {
|
|
280
|
-
return expRow[this._dataViewIdProperty] !== item[this._dataViewIdProperty];
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
if (!isMultipleCollapsing) {
|
|
284
|
-
this.dataView.endUpdate();
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** Expand a row given the dataview item that is to be expanded */
|
|
289
|
-
expandDetailView(item: any) {
|
|
290
|
-
if (this._addonOptions?.singleRowExpand) {
|
|
291
|
-
this.collapseAll();
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
item[`${this._keyPrefix}collapsed`] = false;
|
|
295
|
-
this._expandedRows.push(item);
|
|
296
|
-
|
|
297
|
-
// In the case something went wrong loading it the first time such a scroll of screen before loaded
|
|
298
|
-
if (!item[`${this._keyPrefix}detailContent`]) {
|
|
299
|
-
item[`${this._keyPrefix}detailViewLoaded`] = false;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// display pre-loading template
|
|
303
|
-
if (!item[`${this._keyPrefix}detailViewLoaded`] || this._addonOptions.loadOnce !== true) {
|
|
304
|
-
item[`${this._keyPrefix}detailContent`] = this._addonOptions?.preTemplate?.(item);
|
|
305
|
-
} else {
|
|
306
|
-
this.onAsyncResponse.notify({
|
|
307
|
-
item,
|
|
308
|
-
itemDetail: item,
|
|
309
|
-
detailView: item[`${this._keyPrefix}detailContent`],
|
|
310
|
-
grid: this._grid
|
|
311
|
-
});
|
|
312
|
-
this.applyTemplateNewLineHeight(item);
|
|
313
|
-
this.dataView.updateItem(item[this._dataViewIdProperty], item);
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
this.applyTemplateNewLineHeight(item);
|
|
318
|
-
this.dataView.updateItem(item[this._dataViewIdProperty], item);
|
|
319
|
-
|
|
320
|
-
// async server call
|
|
321
|
-
this._addonOptions.process(item);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** Saves the current state of the detail view */
|
|
325
|
-
saveDetailView(item: any) {
|
|
326
|
-
const view = document.querySelector(`.${this.gridUid} .innerDetailView_${item[this._dataViewIdProperty]}`);
|
|
327
|
-
if (view) {
|
|
328
|
-
const html = view.innerHTML;
|
|
329
|
-
if (html !== undefined) {
|
|
330
|
-
item[`${this._keyPrefix}detailContent`] = html;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* subscribe to the onAsyncResponse so that the plugin knows when the user server side calls finished
|
|
337
|
-
* the response has to be as "args.item" (or "args.itemDetail") with it's data back
|
|
338
|
-
*/
|
|
339
|
-
handleOnAsyncResponse(e: SlickEventData, args: { item: any; itemDetail: any; detailView?: any; }) {
|
|
340
|
-
if (!args || (!args.item && !args.itemDetail)) {
|
|
341
|
-
console.error('SlickRowDetailView plugin requires the onAsyncResponse() to supply "args.item" property.');
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// we accept item/itemDetail, just get the one which has data
|
|
346
|
-
const itemDetail = args.item || args.itemDetail;
|
|
347
|
-
|
|
348
|
-
// If we just want to load in a view directly we can use detailView property to do so
|
|
349
|
-
itemDetail[`${this._keyPrefix}detailContent`] = args.detailView ?? this._addonOptions?.postTemplate?.(itemDetail);
|
|
350
|
-
itemDetail[`${this._keyPrefix}detailViewLoaded`] = true;
|
|
351
|
-
this.dataView.updateItem(itemDetail[this._dataViewIdProperty], itemDetail);
|
|
352
|
-
|
|
353
|
-
// trigger an event once the post template is finished loading
|
|
354
|
-
this.onAsyncEndUpdate.notify({
|
|
355
|
-
grid: this._grid,
|
|
356
|
-
item: itemDetail,
|
|
357
|
-
itemDetail,
|
|
358
|
-
}, e, this);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* TODO interface only has a GETTER not a SETTER..why?
|
|
363
|
-
* Override the logic for showing (or not) the expand icon (use case example: only every 2nd row is expandable)
|
|
364
|
-
* Method that user can pass to override the default behavior or making every row an expandable row.
|
|
365
|
-
* In order word, user can choose which rows to be an available row detail (or not) by providing his own logic.
|
|
366
|
-
* @param overrideFn: override function callback
|
|
367
|
-
*/
|
|
368
|
-
expandableOverride(overrideFn: UsabilityOverrideFn) {
|
|
369
|
-
this._expandableOverride = overrideFn;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
getExpandableOverride(): UsabilityOverrideFn | null {
|
|
373
|
-
return this._expandableOverride;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/** Get the Column Definition of the first column dedicated to toggling the Row Detail View */
|
|
377
|
-
getColumnDefinition(): Column {
|
|
378
|
-
const columnId = String(this._addonOptions?.columnId ?? this._defaults.columnId);
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
id: columnId,
|
|
382
|
-
field: columnId,
|
|
383
|
-
name: '',
|
|
384
|
-
alwaysRenderColumn: this._addonOptions?.alwaysRenderColumn,
|
|
385
|
-
cssClass: this._addonOptions.cssClass || '',
|
|
386
|
-
excludeFromExport: true,
|
|
387
|
-
excludeFromColumnPicker: true,
|
|
388
|
-
excludeFromGridMenu: true,
|
|
389
|
-
excludeFromQuery: true,
|
|
390
|
-
excludeFromHeaderMenu: true,
|
|
391
|
-
formatter: this.detailSelectionFormatter.bind(this),
|
|
392
|
-
reorderable: this._addonOptions.reorderable,
|
|
393
|
-
resizable: false,
|
|
394
|
-
sortable: false,
|
|
395
|
-
toolTip: this._addonOptions.toolTip,
|
|
396
|
-
width: this._addonOptions.width,
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/** return the currently expanded rows */
|
|
401
|
-
getExpandedRows(): Array<number | string> {
|
|
402
|
-
return this._expandedRows;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/** return the rows that are out of the viewport */
|
|
406
|
-
getOutOfViewportRows(): Array<number | string> {
|
|
407
|
-
return this._rowIdsOutOfViewport;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/** Takes in the item we are filtering and if it is an expanded row returns it's parents row to filter on */
|
|
411
|
-
getFilterItem(item: any) {
|
|
412
|
-
if (item[`${this._keyPrefix}isPadding`] && item[`${this._keyPrefix}parent`]) {
|
|
413
|
-
item = item[`${this._keyPrefix}parent`];
|
|
414
|
-
}
|
|
415
|
-
return item;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** Resize the Row Detail View */
|
|
419
|
-
resizeDetailView(item: any) {
|
|
420
|
-
if (!item) {
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Grad each of the DOM elements
|
|
425
|
-
const mainContainer = document.querySelector<HTMLDivElement>(`.${this.gridUid} .detailViewContainer_${item[this._dataViewIdProperty]}`);
|
|
426
|
-
const cellItem = document.querySelector<HTMLDivElement>(`.${this.gridUid} .cellDetailView_${item[this._dataViewIdProperty]}`);
|
|
427
|
-
const inner = document.querySelector<HTMLDivElement>(`.${this.gridUid} .innerDetailView_${item[this._dataViewIdProperty]}`);
|
|
428
|
-
|
|
429
|
-
if (!mainContainer || !cellItem || !inner) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
for (let idx = 1; idx <= item[`${this._keyPrefix}sizePadding`]; idx++) {
|
|
434
|
-
this.dataView.deleteItem(`${item[this._dataViewIdProperty]}.${idx}`);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const rowHeight = this.gridOptions.rowHeight as number; // height of a row
|
|
438
|
-
const lineHeight = 13; // we know cuz we wrote the custom css init ;)
|
|
439
|
-
|
|
440
|
-
// remove the height so we can calculate the height
|
|
441
|
-
mainContainer.style.minHeight = '';
|
|
442
|
-
|
|
443
|
-
// Get the scroll height for the main container so we know the actual size of the view
|
|
444
|
-
const itemHeight = mainContainer.scrollHeight;
|
|
445
|
-
|
|
446
|
-
// Now work out how many rows
|
|
447
|
-
const rowCount = Math.ceil(itemHeight / rowHeight);
|
|
448
|
-
|
|
449
|
-
item[`${this._keyPrefix}sizePadding`] = Math.ceil(((rowCount * 2) * lineHeight) / rowHeight);
|
|
450
|
-
item[`${this._keyPrefix}height`] = itemHeight;
|
|
451
|
-
|
|
452
|
-
let outterHeight = (item[`${this._keyPrefix}sizePadding`] * rowHeight);
|
|
453
|
-
if (this._addonOptions.maxRows !== undefined && item[`${this._keyPrefix}sizePadding`] > this._addonOptions.maxRows) {
|
|
454
|
-
outterHeight = this._addonOptions.maxRows! * rowHeight;
|
|
455
|
-
item[`${this._keyPrefix}sizePadding`] = this._addonOptions.maxRows;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// If the padding is now more than the original minRowBuff we need to increase it
|
|
459
|
-
if (this.gridOptions.minRowBuffer! < item[`${this._keyPrefix}sizePadding`]) {
|
|
460
|
-
// Update the minRowBuffer so that the view doesn't disappear when it's at top of screen + the original default 3
|
|
461
|
-
this.gridOptions.minRowBuffer = item[`${this._keyPrefix}sizePadding`] + 3;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
mainContainer.setAttribute('style', `min-height: ${item[this._keyPrefix + 'height']}px`);
|
|
465
|
-
if (cellItem) {
|
|
466
|
-
cellItem.setAttribute('style', `height: ${outterHeight}px; top: ${rowHeight}px`);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const idxParent = this.dataView.getIdxById(item[this._dataViewIdProperty]) as number;
|
|
470
|
-
for (let idx = 1; idx <= item[`${this._keyPrefix}sizePadding`]; idx++) {
|
|
471
|
-
this.dataView.insertItem(idxParent + idx, this.getPaddingItem(item, idx));
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Lastly save the updated state
|
|
475
|
-
this.saveDetailView(item);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// --
|
|
479
|
-
// protected functions
|
|
480
|
-
// ------------------
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* create the detail ctr node. this belongs to the dev & can be custom-styled as per
|
|
484
|
-
*/
|
|
485
|
-
protected applyTemplateNewLineHeight(item: any) {
|
|
486
|
-
// the height is calculated by the template row count (how many line of items does the template view have)
|
|
487
|
-
const rowCount = this._addonOptions.panelRows;
|
|
488
|
-
|
|
489
|
-
// calculate padding requirements based on detail-content..
|
|
490
|
-
// ie. worst-case: create an invisible dom node now & find it's height.
|
|
491
|
-
const lineHeight = 13; // we know cuz we wrote the custom css init ;)
|
|
492
|
-
item[`${this._keyPrefix}sizePadding`] = Math.ceil(((rowCount * 2) * lineHeight) / this.gridOptions.rowHeight!);
|
|
493
|
-
item[`${this._keyPrefix}height`] = (item[`${this._keyPrefix}sizePadding`] * this.gridOptions.rowHeight!);
|
|
494
|
-
const idxParent = this.dataView.getIdxById(item[this._dataViewIdProperty]);
|
|
495
|
-
for (let idx = 1; idx <= item[`${this._keyPrefix}sizePadding`]; idx++) {
|
|
496
|
-
this.dataView.insertItem((idxParent || 0) + idx, this.getPaddingItem(item, idx));
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
protected calculateOutOfRangeViews() {
|
|
501
|
-
if (this._grid) {
|
|
502
|
-
let scrollDir: 'UP' | 'DOWN';
|
|
503
|
-
const renderedRange = this._grid.getRenderedRange();
|
|
504
|
-
// Only check if we have expanded rows
|
|
505
|
-
if (this._expandedRows.length > 0) {
|
|
506
|
-
// Assume scroll direction is down by default.
|
|
507
|
-
scrollDir = 'DOWN';
|
|
508
|
-
if (this._lastRange) {
|
|
509
|
-
// Some scrolling isn't anything as the range is the same
|
|
510
|
-
if (this._lastRange.top === renderedRange.top && this._lastRange.bottom === renderedRange.bottom) {
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// If our new top is smaller we are scrolling up
|
|
515
|
-
if (this._lastRange.top > renderedRange.top ||
|
|
516
|
-
// Or we are at very top but our bottom is increasing
|
|
517
|
-
(this._lastRange.top === 0 && renderedRange.top === 0 && (this._lastRange.bottom > renderedRange.bottom))) {
|
|
518
|
-
scrollDir = 'UP';
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
this._expandedRows.forEach((row) => {
|
|
524
|
-
const rowIndex = this.dataView.getRowById(row[this._dataViewIdProperty]) as number;
|
|
525
|
-
const rowPadding = row[`${this._keyPrefix}sizePadding`];
|
|
526
|
-
const isRowOutOfRange = this._rowIdsOutOfViewport.some(rowId => rowId === row[this._dataViewIdProperty]);
|
|
527
|
-
|
|
528
|
-
if (scrollDir === 'UP') {
|
|
529
|
-
// save the view when asked
|
|
530
|
-
if (this._addonOptions.saveDetailViewOnScroll) {
|
|
531
|
-
// If the bottom item within buffer range is an expanded row save it.
|
|
532
|
-
if (rowIndex >= renderedRange.bottom - this._gridRowBuffer) {
|
|
533
|
-
this.saveDetailView(row);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// If the row expanded area is within the buffer notify that it is back in range
|
|
538
|
-
if (isRowOutOfRange && ((rowIndex - this._outsideRange) < renderedRange.top) && (rowIndex >= renderedRange.top)) {
|
|
539
|
-
this.notifyBackToViewportWhenDomExist(row, row[this._dataViewIdProperty]);
|
|
540
|
-
} else if (!isRowOutOfRange && ((rowIndex + rowPadding) > renderedRange.bottom)) {
|
|
541
|
-
// if our first expanded row is about to go off the bottom
|
|
542
|
-
this.notifyOutOfViewport(row, row[this._dataViewIdProperty]);
|
|
543
|
-
}
|
|
544
|
-
} else if (scrollDir === 'DOWN') {
|
|
545
|
-
// save the view when asked
|
|
546
|
-
if (this._addonOptions.saveDetailViewOnScroll) {
|
|
547
|
-
// If the top item within buffer range is an expanded row save it.
|
|
548
|
-
if (rowIndex <= renderedRange.top + this._gridRowBuffer) {
|
|
549
|
-
this.saveDetailView(row);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// If row index is i higher than bottom with some added value (To ignore top rows off view) and is with view and was our of range
|
|
554
|
-
if (isRowOutOfRange && ((rowIndex + rowPadding + this._outsideRange) > renderedRange.bottom) && (rowIndex < (rowIndex + rowPadding))) {
|
|
555
|
-
this.notifyBackToViewportWhenDomExist(row, row[this._dataViewIdProperty]);
|
|
556
|
-
} else if (!isRowOutOfRange && (rowIndex < renderedRange.top)) {
|
|
557
|
-
// if our row is outside top of and the buffering zone but not in the array of outOfVisable range notify it
|
|
558
|
-
this.notifyOutOfViewport(row, row[this._dataViewIdProperty]);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
});
|
|
562
|
-
this._lastRange = renderedRange;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
protected calculateOutOfRangeViewsSimplerVersion() {
|
|
567
|
-
if (this._grid) {
|
|
568
|
-
const renderedRange = this._grid.getRenderedRange();
|
|
569
|
-
|
|
570
|
-
this._expandedRows.forEach((row) => {
|
|
571
|
-
const rowIndex = this.dataView.getRowById(row[this._dataViewIdProperty]) as number;
|
|
572
|
-
const isOutOfVisibility = this.checkIsRowOutOfViewportRange(rowIndex, renderedRange);
|
|
573
|
-
if (!isOutOfVisibility && this._rowIdsOutOfViewport.some(rowId => rowId === row[this._dataViewIdProperty])) {
|
|
574
|
-
this.notifyBackToViewportWhenDomExist(row, row[this._dataViewIdProperty]);
|
|
575
|
-
} else if (isOutOfVisibility) {
|
|
576
|
-
this.notifyOutOfViewport(row, row[this._dataViewIdProperty]);
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
protected checkExpandableOverride(row: number, dataContext: any, grid: SlickGrid) {
|
|
583
|
-
if (typeof this._expandableOverride === 'function') {
|
|
584
|
-
return this._expandableOverride(row, dataContext, grid);
|
|
585
|
-
}
|
|
586
|
-
return true;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
protected checkIsRowOutOfViewportRange(rowIndex: number, renderedRange: any) {
|
|
590
|
-
return (Math.abs(renderedRange.bottom - this._gridRowBuffer - rowIndex) > this._visibleRenderedCellCount * 2);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/** Get the Row Detail padding (which are the rows dedicated to the detail panel) */
|
|
594
|
-
protected getPaddingItem(parent: any, offset: any) {
|
|
595
|
-
const item: any = {};
|
|
596
|
-
|
|
597
|
-
Object.keys(this.dataView).forEach(prop => {
|
|
598
|
-
if (prop) {
|
|
599
|
-
item[prop] = null;
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
item[this._dataViewIdProperty] = `${parent[this._dataViewIdProperty]}.${offset}`;
|
|
603
|
-
|
|
604
|
-
// additional hidden padding metadata fields
|
|
605
|
-
item[`${this._keyPrefix}collapsed`] = true;
|
|
606
|
-
item[`${this._keyPrefix}isPadding`] = true;
|
|
607
|
-
item[`${this._keyPrefix}parent`] = parent;
|
|
608
|
-
item[`${this._keyPrefix}offset`] = offset;
|
|
609
|
-
|
|
610
|
-
return item;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/** The Formatter of the toggling icon of the Row Detail */
|
|
614
|
-
protected detailSelectionFormatter(row: number, _cell: number, _val: any, _colDef: Column, dataContext: any, grid: SlickGrid): FormatterResultWithHtml | HTMLElement | '' {
|
|
615
|
-
if (!this.checkExpandableOverride(row, dataContext, grid)) {
|
|
616
|
-
return '';
|
|
617
|
-
} else {
|
|
618
|
-
if (dataContext[`${this._keyPrefix}collapsed`] === undefined) {
|
|
619
|
-
dataContext[`${this._keyPrefix}collapsed`] = true;
|
|
620
|
-
dataContext[`${this._keyPrefix}sizePadding`] = 0; // the required number of pading rows
|
|
621
|
-
dataContext[`${this._keyPrefix}height`] = 0; // the actual height in pixels of the detail field
|
|
622
|
-
dataContext[`${this._keyPrefix}isPadding`] = false;
|
|
623
|
-
dataContext[`${this._keyPrefix}parent`] = undefined;
|
|
624
|
-
dataContext[`${this._keyPrefix}offset`] = 0;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
if (dataContext[`${this._keyPrefix}isPadding`]) {
|
|
628
|
-
// render nothing
|
|
629
|
-
} else if (dataContext[`${this._keyPrefix}collapsed`]) {
|
|
630
|
-
let collapsedClasses = `${this._addonOptions.cssClass || ''} expand `;
|
|
631
|
-
if (this._addonOptions.collapsedClass) {
|
|
632
|
-
collapsedClasses += this._addonOptions.collapsedClass;
|
|
633
|
-
}
|
|
634
|
-
return createDomElement('div', { className: classNameToList(collapsedClasses).join(' ') });
|
|
635
|
-
} else {
|
|
636
|
-
const rowHeight = this.gridOptions.rowHeight || 0;
|
|
637
|
-
let outterHeight = (dataContext[`${this._keyPrefix}sizePadding`] || 0) * this.gridOptions.rowHeight!;
|
|
638
|
-
|
|
639
|
-
if (this._addonOptions.maxRows !== null && ((dataContext[`${this._keyPrefix}sizePadding`] || 0) > this._addonOptions.maxRows!)) {
|
|
640
|
-
outterHeight = this._addonOptions.maxRows! * rowHeight!;
|
|
641
|
-
dataContext[`${this._keyPrefix}sizePadding`] = this._addonOptions.maxRows;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// sneaky extra </div> inserted here-----------------v
|
|
645
|
-
let expandedClasses = `${this._addonOptions.cssClass || ''} collapse `;
|
|
646
|
-
if (this._addonOptions.expandedClass) {
|
|
647
|
-
expandedClasses += this._addonOptions.expandedClass;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// create the Row Detail div container that will be inserted AFTER the `.slick-cell`
|
|
651
|
-
const cellDetailContainerElm = createDomElement('div', {
|
|
652
|
-
className: `dynamic-cell-detail cellDetailView_${dataContext[this._dataViewIdProperty]}`,
|
|
653
|
-
style: { height: `${outterHeight}px`, top: `${rowHeight}px` }
|
|
654
|
-
});
|
|
655
|
-
const innerContainerElm = createDomElement('div', { className: `detail-container detailViewContainer_${dataContext[this._dataViewIdProperty]}` });
|
|
656
|
-
const innerDetailViewElm = createDomElement('div', { className: `innerDetailView_${dataContext[this._dataViewIdProperty]}` });
|
|
657
|
-
if (dataContext[`${this._keyPrefix}detailContent`] instanceof HTMLElement) {
|
|
658
|
-
innerDetailViewElm.appendChild(dataContext[`${this._keyPrefix}detailContent`]);
|
|
659
|
-
} else {
|
|
660
|
-
innerDetailViewElm.innerHTML = this._grid.sanitizeHtmlString(dataContext[`${this._keyPrefix}detailContent`]);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
innerContainerElm.appendChild(innerDetailViewElm);
|
|
664
|
-
cellDetailContainerElm.appendChild(innerContainerElm);
|
|
665
|
-
|
|
666
|
-
const result: FormatterResultWithHtml = {
|
|
667
|
-
html: createDomElement('div', { className: classNameToList(expandedClasses).join(' ') }),
|
|
668
|
-
insertElementAfterTarget: cellDetailContainerElm,
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
return result;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
return '';
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/** When row is getting toggled, we will handle the action of collapsing/expanding */
|
|
678
|
-
protected handleAccordionShowHide(item: any) {
|
|
679
|
-
if (item) {
|
|
680
|
-
if (!item[`${this._keyPrefix}collapsed`]) {
|
|
681
|
-
this.collapseDetailView(item);
|
|
682
|
-
} else {
|
|
683
|
-
this.expandDetailView(item);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/** Handle mouse click event */
|
|
689
|
-
protected handleClick(e: SlickEventData, args: { row: number; cell: number; }) {
|
|
690
|
-
const dataContext = this._grid.getDataItem(args.row);
|
|
691
|
-
|
|
692
|
-
if (this.checkExpandableOverride(args.row, dataContext, this._grid)) {
|
|
693
|
-
// clicking on a row select checkbox
|
|
694
|
-
const columnDef = this._grid.getColumns()[args.cell];
|
|
695
|
-
if (this._addonOptions.useRowClick || (columnDef.id === this._addonOptions.columnId && e.target!.classList.contains(this._addonOptions.cssClass || ''))) {
|
|
696
|
-
// if editing, try to commit
|
|
697
|
-
if (this._grid.getEditorLock().isActive() && !this._grid.getEditorLock().commitCurrentEdit()) {
|
|
698
|
-
e.preventDefault();
|
|
699
|
-
e.stopImmediatePropagation();
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// trigger an event before toggling
|
|
704
|
-
// user could cancel the Row Detail opening when event is returning false
|
|
705
|
-
const ignorePrevEventDataValue = true; // click event might return false from Row Selection canCellBeActive() validation, we need to ignore that
|
|
706
|
-
if (this.onBeforeRowDetailToggle.notify({ grid: this._grid, item: dataContext }, e, this, ignorePrevEventDataValue).getReturnValue() === false) {
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
this.toggleRowSelection(args.row, dataContext);
|
|
711
|
-
|
|
712
|
-
// trigger an event after toggling
|
|
713
|
-
this.onAfterRowDetailToggle.notify({
|
|
714
|
-
grid: this._grid,
|
|
715
|
-
item: dataContext,
|
|
716
|
-
expandedRows: this._expandedRows,
|
|
717
|
-
}, e, this);
|
|
718
|
-
|
|
719
|
-
e.stopPropagation();
|
|
720
|
-
e.stopImmediatePropagation();
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
protected handleScroll() {
|
|
726
|
-
if (this._addonOptions.useSimpleViewportCalc) {
|
|
727
|
-
this.calculateOutOfRangeViewsSimplerVersion();
|
|
728
|
-
} else {
|
|
729
|
-
this.calculateOutOfRangeViews();
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
protected notifyOutOfViewport(item: any, rowId: number | string) {
|
|
734
|
-
const rowIndex = item.rowIndex || this.dataView.getRowById(item[this._dataViewIdProperty]);
|
|
735
|
-
|
|
736
|
-
this.onRowOutOfViewportRange.notify({
|
|
737
|
-
grid: this._grid,
|
|
738
|
-
item,
|
|
739
|
-
rowId,
|
|
740
|
-
rowIndex,
|
|
741
|
-
expandedRows: this._expandedRows,
|
|
742
|
-
rowIdsOutOfViewport: this.syncOutOfViewportArray(rowId, true)
|
|
743
|
-
}, null, this);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
protected notifyBackToViewportWhenDomExist(item: any, rowId: number | string) {
|
|
747
|
-
const rowIndex = item.rowIndex || this.dataView.getRowById(item[this._dataViewIdProperty]);
|
|
748
|
-
|
|
749
|
-
setTimeout(() => {
|
|
750
|
-
// make sure View Row DOM Element really exist before notifying that it's a row that is visible again
|
|
751
|
-
if (document.querySelector(`.${this.gridUid} .cellDetailView_${item[this._dataViewIdProperty]}`)) {
|
|
752
|
-
this.onRowBackToViewportRange.notify({
|
|
753
|
-
grid: this._grid,
|
|
754
|
-
item,
|
|
755
|
-
rowId,
|
|
756
|
-
rowIndex,
|
|
757
|
-
expandedRows: this._expandedRows,
|
|
758
|
-
rowIdsOutOfViewport: this.syncOutOfViewportArray(rowId, false)
|
|
759
|
-
}, null, this);
|
|
760
|
-
}
|
|
761
|
-
}, 100);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
protected syncOutOfViewportArray(rowId: number | string, isAdding: boolean) {
|
|
765
|
-
const arrayRowIndex = this._rowIdsOutOfViewport.findIndex(outOfViewportRowId => outOfViewportRowId === rowId);
|
|
766
|
-
|
|
767
|
-
if (isAdding && arrayRowIndex < 0) {
|
|
768
|
-
this._rowIdsOutOfViewport.push(rowId);
|
|
769
|
-
} else if (!isAdding && arrayRowIndex >= 0) {
|
|
770
|
-
this._rowIdsOutOfViewport.splice(arrayRowIndex, 1);
|
|
771
|
-
}
|
|
772
|
-
return this._rowIdsOutOfViewport;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
protected toggleRowSelection(rowNumber: number, dataContext: any) {
|
|
776
|
-
if (this.checkExpandableOverride(rowNumber, dataContext, this._grid)) {
|
|
777
|
-
this.dataView.beginUpdate();
|
|
778
|
-
this.handleAccordionShowHide(dataContext);
|
|
779
|
-
this.dataView.endUpdate();
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
}
|
|
1
|
+
import { createDomElement, SlickEvent, SlickEventHandler, Utils as SlickUtils } from '@slickgrid-universal/common';
|
|
2
|
+
import type {
|
|
3
|
+
Column,
|
|
4
|
+
ExternalResource,
|
|
5
|
+
FormatterResultWithHtml,
|
|
6
|
+
GridOption,
|
|
7
|
+
OnAfterRowDetailToggleArgs,
|
|
8
|
+
OnBeforeRowDetailToggleArgs,
|
|
9
|
+
OnRowBackToViewportRangeArgs,
|
|
10
|
+
OnRowDetailAsyncEndUpdateArgs,
|
|
11
|
+
OnRowDetailAsyncResponseArgs,
|
|
12
|
+
OnRowOutOfViewportRangeArgs,
|
|
13
|
+
PubSubService,
|
|
14
|
+
RowDetailView,
|
|
15
|
+
RowDetailViewOption,
|
|
16
|
+
SlickGrid,
|
|
17
|
+
SlickRowDetailView as UniversalRowDetailView,
|
|
18
|
+
SlickDataView,
|
|
19
|
+
SlickEventData,
|
|
20
|
+
UsabilityOverrideFn,
|
|
21
|
+
} from '@slickgrid-universal/common';
|
|
22
|
+
import { classNameToList, extend } from '@slickgrid-universal/utils';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A plugin to add Row Detail Panel View (for example providing order detail info when clicking on the order row in the grid)
|
|
26
|
+
* Original StackOverflow question & article making this possible (thanks to violet313)
|
|
27
|
+
* https://stackoverflow.com/questions/10535164/can-slickgrids-row-height-be-dynamically-altered#29399927
|
|
28
|
+
* http://violet313.org/slickgrids/#intro
|
|
29
|
+
*/
|
|
30
|
+
export class SlickRowDetailView implements ExternalResource, UniversalRowDetailView {
|
|
31
|
+
// --
|
|
32
|
+
// public API
|
|
33
|
+
pluginName = 'RowDetailView' as const;
|
|
34
|
+
|
|
35
|
+
/** Fired when the async response finished */
|
|
36
|
+
onAsyncEndUpdate = new SlickEvent<OnRowDetailAsyncEndUpdateArgs>('onAsyncEndUpdate');
|
|
37
|
+
|
|
38
|
+
/** This event must be used with the "notify" by the end user once the Asynchronous Server call returns the item detail */
|
|
39
|
+
onAsyncResponse = new SlickEvent<OnRowDetailAsyncResponseArgs>('onAsyncResponse');
|
|
40
|
+
|
|
41
|
+
/** Fired after the row detail gets toggled */
|
|
42
|
+
onAfterRowDetailToggle = new SlickEvent<OnAfterRowDetailToggleArgs>('onAfterRowDetailToggle');
|
|
43
|
+
|
|
44
|
+
/** Fired before the row detail gets toggled */
|
|
45
|
+
onBeforeRowDetailToggle = new SlickEvent<OnBeforeRowDetailToggleArgs>('onBeforeRowDetailToggle');
|
|
46
|
+
|
|
47
|
+
/** Fired after the row detail gets toggled */
|
|
48
|
+
onRowBackToViewportRange = new SlickEvent<OnRowBackToViewportRangeArgs>('onRowBackToViewportRange');
|
|
49
|
+
|
|
50
|
+
/** Fired after a row becomes out of viewport range (when user can't see the row anymore) */
|
|
51
|
+
onRowOutOfViewportRange = new SlickEvent<OnRowOutOfViewportRangeArgs>('onRowOutOfViewportRange');
|
|
52
|
+
|
|
53
|
+
// --
|
|
54
|
+
// protected props
|
|
55
|
+
protected _addonOptions!: RowDetailView;
|
|
56
|
+
protected _dataViewIdProperty = 'id';
|
|
57
|
+
protected _eventHandler: SlickEventHandler;
|
|
58
|
+
protected _expandableOverride: UsabilityOverrideFn | null = null;
|
|
59
|
+
protected _expandedRows: any[] = [];
|
|
60
|
+
protected _grid!: SlickGrid;
|
|
61
|
+
protected _gridRowBuffer = 0;
|
|
62
|
+
protected _gridUid = '';
|
|
63
|
+
protected _keyPrefix = '';
|
|
64
|
+
protected _lastRange: { bottom: number; top: number; } | null = null;
|
|
65
|
+
protected _outsideRange = 5;
|
|
66
|
+
protected _rowIdsOutOfViewport: Array<number | string> = [];
|
|
67
|
+
protected _visibleRenderedCellCount = 0;
|
|
68
|
+
protected _defaults = {
|
|
69
|
+
alwaysRenderColumn: true,
|
|
70
|
+
columnId: '_detail_selector',
|
|
71
|
+
field: '_detail_selector',
|
|
72
|
+
cssClass: 'detailView-toggle',
|
|
73
|
+
collapseAllOnSort: true,
|
|
74
|
+
collapsedClass: undefined,
|
|
75
|
+
expandedClass: undefined,
|
|
76
|
+
keyPrefix: '_',
|
|
77
|
+
loadOnce: false,
|
|
78
|
+
maxRows: undefined,
|
|
79
|
+
reorderable: false,
|
|
80
|
+
saveDetailViewOnScroll: true,
|
|
81
|
+
singleRowExpand: false,
|
|
82
|
+
useSimpleViewportCalc: false,
|
|
83
|
+
toolTip: '',
|
|
84
|
+
width: 30,
|
|
85
|
+
} as unknown as RowDetailView;
|
|
86
|
+
|
|
87
|
+
/** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */
|
|
88
|
+
constructor(protected readonly pubSubService: PubSubService) {
|
|
89
|
+
this._eventHandler = new SlickEventHandler();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get addonOptions() {
|
|
93
|
+
return this._addonOptions;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Getter of SlickGrid DataView object */
|
|
97
|
+
get dataView(): SlickDataView {
|
|
98
|
+
return this._grid?.getData<SlickDataView>();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get dataViewIdProperty(): string {
|
|
102
|
+
return this._dataViewIdProperty;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get eventHandler() {
|
|
106
|
+
return this._eventHandler;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Getter for the Grid Options pulled through the Grid Object */
|
|
110
|
+
get gridOptions(): GridOption {
|
|
111
|
+
return this._grid?.getOptions() || {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get gridUid() {
|
|
115
|
+
return this._gridUid || (this._grid?.getUID() || '');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
set lastRange(range: { bottom: number; top: number; }) {
|
|
119
|
+
this._lastRange = range;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
set rowIdsOutOfViewport(rowIds: Array<string | number>) {
|
|
123
|
+
this._rowIdsOutOfViewport = rowIds;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get visibleRenderedCellCount(): number {
|
|
127
|
+
return this._visibleRenderedCellCount;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Initialize the Export Service
|
|
132
|
+
* @param _grid
|
|
133
|
+
* @param _containerService
|
|
134
|
+
*/
|
|
135
|
+
init(grid: SlickGrid) {
|
|
136
|
+
this._grid = grid;
|
|
137
|
+
if (!grid) {
|
|
138
|
+
throw new Error('[Slickgrid-Universal] RowDetailView Plugin requires the Grid instance to be passed as argument to the "init()" method.');
|
|
139
|
+
}
|
|
140
|
+
this._grid = grid;
|
|
141
|
+
this._gridUid = grid.getUID();
|
|
142
|
+
if (!this._addonOptions) {
|
|
143
|
+
this._addonOptions = extend(true, {}, this._defaults, this.gridOptions.rowDetailView) as RowDetailView;
|
|
144
|
+
}
|
|
145
|
+
this._keyPrefix = this._addonOptions?.keyPrefix || '_';
|
|
146
|
+
|
|
147
|
+
// add PubSub instance to all SlickEvent
|
|
148
|
+
SlickUtils.addSlickEventPubSubWhenDefined(this.pubSubService, this);
|
|
149
|
+
|
|
150
|
+
// Update the minRowBuffer so that the view doesn't disappear when it's at top of screen + the original default 3
|
|
151
|
+
this._gridRowBuffer = this.gridOptions.minRowBuffer || 0;
|
|
152
|
+
this.gridOptions.minRowBuffer = this._addonOptions.panelRows + 3;
|
|
153
|
+
|
|
154
|
+
this._eventHandler
|
|
155
|
+
.subscribe(this._grid.onClick, this.handleClick.bind(this))
|
|
156
|
+
.subscribe(this._grid.onBeforeEditCell, () => this.collapseAll())
|
|
157
|
+
.subscribe(this._grid.onScroll, this.handleScroll.bind(this));
|
|
158
|
+
|
|
159
|
+
// Sort will, by default, Collapse all of the open items (unless user implements his own onSort which deals with open row and padding)
|
|
160
|
+
if (this._addonOptions.collapseAllOnSort) {
|
|
161
|
+
// sort event can be triggered by column header click or from header menu
|
|
162
|
+
this.pubSubService.subscribe('onSortChanged', () => this.collapseAll());
|
|
163
|
+
this._expandedRows = [];
|
|
164
|
+
this._rowIdsOutOfViewport = [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this._eventHandler.subscribe(this.dataView.onRowCountChanged, () => {
|
|
168
|
+
this._grid.updateRowCount();
|
|
169
|
+
this._grid.render();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
this._eventHandler.subscribe(this.dataView.onRowsChanged, (_e: SlickEventData, args: { rows: number[]; }) => {
|
|
173
|
+
this._grid.invalidateRows(args.rows);
|
|
174
|
+
this._grid.render();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// subscribe to the onAsyncResponse so that the plugin knows when the user server side calls finished
|
|
178
|
+
this._eventHandler.subscribe(this.onAsyncResponse, this.handleOnAsyncResponse.bind(this));
|
|
179
|
+
|
|
180
|
+
// after data is set, let's get the DataView Id Property name used (defaults to "id")
|
|
181
|
+
this._eventHandler.subscribe(this.dataView.onSetItemsCalled, () => {
|
|
182
|
+
this._dataViewIdProperty = this.dataView?.getIdPropertyName() || 'id';
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// if we use the alternative & simpler calculation of the out of viewport range
|
|
186
|
+
// we will need to know how many rows are rendered on the screen and we need to wait for grid to be rendered
|
|
187
|
+
// unfortunately there is no triggered event for knowing when grid is finished, so we use 250ms delay and it's typically more than enough
|
|
188
|
+
if (this._addonOptions.useSimpleViewportCalc) {
|
|
189
|
+
this._eventHandler.subscribe(this._grid.onRendered, (_e: SlickEventData, args: { endRow: number; startRow: number; }) => {
|
|
190
|
+
if (args?.endRow) {
|
|
191
|
+
this._visibleRenderedCellCount = args.endRow - args.startRow;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Dispose of the Slick Row Detail View */
|
|
198
|
+
dispose() {
|
|
199
|
+
this._eventHandler?.unsubscribeAll();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
create(columnDefinitions: Column[], gridOptions: GridOption): UniversalRowDetailView | null {
|
|
203
|
+
if (!gridOptions.rowDetailView) {
|
|
204
|
+
throw new Error('[Slickgrid-Universal] The Row Detail View requires options to be passed via the "rowDetailView" property of the Grid Options');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this._addonOptions = extend(true, {}, this._defaults, gridOptions.rowDetailView) as RowDetailView;
|
|
208
|
+
|
|
209
|
+
// user could override the expandable icon logic from within the options or after instantiating the plugin
|
|
210
|
+
if (typeof this._addonOptions.expandableOverride === 'function') {
|
|
211
|
+
this.expandableOverride(this._addonOptions.expandableOverride);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (Array.isArray(columnDefinitions) && gridOptions) {
|
|
215
|
+
const newRowDetailViewColumn: Column = this.getColumnDefinition();
|
|
216
|
+
|
|
217
|
+
// add new row detail column unless it was already added
|
|
218
|
+
if (!columnDefinitions.some(col => col.id === newRowDetailViewColumn.id)) {
|
|
219
|
+
const rowDetailColDef = Array.isArray(columnDefinitions) && columnDefinitions.find(col => col?.behavior === 'selectAndMove');
|
|
220
|
+
const finalRowDetailViewColumn = rowDetailColDef ? rowDetailColDef : newRowDetailViewColumn;
|
|
221
|
+
|
|
222
|
+
// column index position in the grid
|
|
223
|
+
const columnPosition = gridOptions?.rowDetailView?.columnIndexPosition ?? 0;
|
|
224
|
+
if (columnPosition > 0) {
|
|
225
|
+
columnDefinitions.splice(columnPosition, 0, finalRowDetailViewColumn);
|
|
226
|
+
} else {
|
|
227
|
+
columnDefinitions.unshift(finalRowDetailViewColumn);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.pubSubService.publish(`onPluginColumnsChanged`, {
|
|
231
|
+
columns: columnDefinitions,
|
|
232
|
+
pluginName: this.pluginName
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return this as unknown as UniversalRowDetailView;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Get current plugin options */
|
|
240
|
+
getOptions(): RowDetailViewOption {
|
|
241
|
+
return this._addonOptions;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** set or change some of the plugin options */
|
|
245
|
+
setOptions(options: Partial<RowDetailViewOption>) {
|
|
246
|
+
this._addonOptions = extend(true, {}, this._addonOptions, options) as RowDetailView;
|
|
247
|
+
if (this._addonOptions?.singleRowExpand) {
|
|
248
|
+
this.collapseAll();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Collapse all of the open items */
|
|
253
|
+
collapseAll() {
|
|
254
|
+
this.dataView.beginUpdate();
|
|
255
|
+
this._expandedRows.forEach(expandedRow => {
|
|
256
|
+
this.collapseDetailView(expandedRow, true);
|
|
257
|
+
});
|
|
258
|
+
this.dataView.endUpdate();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Colapse an Item so it is not longer seen */
|
|
262
|
+
collapseDetailView(item: any, isMultipleCollapsing = false) {
|
|
263
|
+
if (!isMultipleCollapsing) {
|
|
264
|
+
this.dataView.beginUpdate();
|
|
265
|
+
}
|
|
266
|
+
// Save the details on the collapse assuming onetime loading
|
|
267
|
+
if (this._addonOptions.loadOnce) {
|
|
268
|
+
this.saveDetailView(item);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
item[`${this._keyPrefix}collapsed`] = true;
|
|
272
|
+
for (let idx = 1; idx <= item[`${this._keyPrefix}sizePadding`]; idx++) {
|
|
273
|
+
this.dataView.deleteItem(`${item[this._dataViewIdProperty]}.${idx}`);
|
|
274
|
+
}
|
|
275
|
+
item[`${this._keyPrefix}sizePadding`] = 0;
|
|
276
|
+
this.dataView.updateItem(item[this._dataViewIdProperty], item);
|
|
277
|
+
|
|
278
|
+
// Remove the item from the expandedRows
|
|
279
|
+
this._expandedRows = this._expandedRows.filter((expRow) => {
|
|
280
|
+
return expRow[this._dataViewIdProperty] !== item[this._dataViewIdProperty];
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (!isMultipleCollapsing) {
|
|
284
|
+
this.dataView.endUpdate();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Expand a row given the dataview item that is to be expanded */
|
|
289
|
+
expandDetailView(item: any) {
|
|
290
|
+
if (this._addonOptions?.singleRowExpand) {
|
|
291
|
+
this.collapseAll();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
item[`${this._keyPrefix}collapsed`] = false;
|
|
295
|
+
this._expandedRows.push(item);
|
|
296
|
+
|
|
297
|
+
// In the case something went wrong loading it the first time such a scroll of screen before loaded
|
|
298
|
+
if (!item[`${this._keyPrefix}detailContent`]) {
|
|
299
|
+
item[`${this._keyPrefix}detailViewLoaded`] = false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// display pre-loading template
|
|
303
|
+
if (!item[`${this._keyPrefix}detailViewLoaded`] || this._addonOptions.loadOnce !== true) {
|
|
304
|
+
item[`${this._keyPrefix}detailContent`] = this._addonOptions?.preTemplate?.(item);
|
|
305
|
+
} else {
|
|
306
|
+
this.onAsyncResponse.notify({
|
|
307
|
+
item,
|
|
308
|
+
itemDetail: item,
|
|
309
|
+
detailView: item[`${this._keyPrefix}detailContent`],
|
|
310
|
+
grid: this._grid
|
|
311
|
+
});
|
|
312
|
+
this.applyTemplateNewLineHeight(item);
|
|
313
|
+
this.dataView.updateItem(item[this._dataViewIdProperty], item);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.applyTemplateNewLineHeight(item);
|
|
318
|
+
this.dataView.updateItem(item[this._dataViewIdProperty], item);
|
|
319
|
+
|
|
320
|
+
// async server call
|
|
321
|
+
this._addonOptions.process(item);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Saves the current state of the detail view */
|
|
325
|
+
saveDetailView(item: any) {
|
|
326
|
+
const view = document.querySelector(`.${this.gridUid} .innerDetailView_${item[this._dataViewIdProperty]}`);
|
|
327
|
+
if (view) {
|
|
328
|
+
const html = view.innerHTML;
|
|
329
|
+
if (html !== undefined) {
|
|
330
|
+
item[`${this._keyPrefix}detailContent`] = html;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* subscribe to the onAsyncResponse so that the plugin knows when the user server side calls finished
|
|
337
|
+
* the response has to be as "args.item" (or "args.itemDetail") with it's data back
|
|
338
|
+
*/
|
|
339
|
+
handleOnAsyncResponse(e: SlickEventData, args: { item: any; itemDetail: any; detailView?: any; }) {
|
|
340
|
+
if (!args || (!args.item && !args.itemDetail)) {
|
|
341
|
+
console.error('SlickRowDetailView plugin requires the onAsyncResponse() to supply "args.item" property.');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// we accept item/itemDetail, just get the one which has data
|
|
346
|
+
const itemDetail = args.item || args.itemDetail;
|
|
347
|
+
|
|
348
|
+
// If we just want to load in a view directly we can use detailView property to do so
|
|
349
|
+
itemDetail[`${this._keyPrefix}detailContent`] = args.detailView ?? this._addonOptions?.postTemplate?.(itemDetail);
|
|
350
|
+
itemDetail[`${this._keyPrefix}detailViewLoaded`] = true;
|
|
351
|
+
this.dataView.updateItem(itemDetail[this._dataViewIdProperty], itemDetail);
|
|
352
|
+
|
|
353
|
+
// trigger an event once the post template is finished loading
|
|
354
|
+
this.onAsyncEndUpdate.notify({
|
|
355
|
+
grid: this._grid,
|
|
356
|
+
item: itemDetail,
|
|
357
|
+
itemDetail,
|
|
358
|
+
}, e, this);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* TODO interface only has a GETTER not a SETTER..why?
|
|
363
|
+
* Override the logic for showing (or not) the expand icon (use case example: only every 2nd row is expandable)
|
|
364
|
+
* Method that user can pass to override the default behavior or making every row an expandable row.
|
|
365
|
+
* In order word, user can choose which rows to be an available row detail (or not) by providing his own logic.
|
|
366
|
+
* @param overrideFn: override function callback
|
|
367
|
+
*/
|
|
368
|
+
expandableOverride(overrideFn: UsabilityOverrideFn) {
|
|
369
|
+
this._expandableOverride = overrideFn;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
getExpandableOverride(): UsabilityOverrideFn | null {
|
|
373
|
+
return this._expandableOverride;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Get the Column Definition of the first column dedicated to toggling the Row Detail View */
|
|
377
|
+
getColumnDefinition(): Column {
|
|
378
|
+
const columnId = String(this._addonOptions?.columnId ?? this._defaults.columnId);
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
id: columnId,
|
|
382
|
+
field: columnId,
|
|
383
|
+
name: '',
|
|
384
|
+
alwaysRenderColumn: this._addonOptions?.alwaysRenderColumn,
|
|
385
|
+
cssClass: this._addonOptions.cssClass || '',
|
|
386
|
+
excludeFromExport: true,
|
|
387
|
+
excludeFromColumnPicker: true,
|
|
388
|
+
excludeFromGridMenu: true,
|
|
389
|
+
excludeFromQuery: true,
|
|
390
|
+
excludeFromHeaderMenu: true,
|
|
391
|
+
formatter: this.detailSelectionFormatter.bind(this),
|
|
392
|
+
reorderable: this._addonOptions.reorderable,
|
|
393
|
+
resizable: false,
|
|
394
|
+
sortable: false,
|
|
395
|
+
toolTip: this._addonOptions.toolTip,
|
|
396
|
+
width: this._addonOptions.width,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** return the currently expanded rows */
|
|
401
|
+
getExpandedRows(): Array<number | string> {
|
|
402
|
+
return this._expandedRows;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** return the rows that are out of the viewport */
|
|
406
|
+
getOutOfViewportRows(): Array<number | string> {
|
|
407
|
+
return this._rowIdsOutOfViewport;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Takes in the item we are filtering and if it is an expanded row returns it's parents row to filter on */
|
|
411
|
+
getFilterItem(item: any) {
|
|
412
|
+
if (item[`${this._keyPrefix}isPadding`] && item[`${this._keyPrefix}parent`]) {
|
|
413
|
+
item = item[`${this._keyPrefix}parent`];
|
|
414
|
+
}
|
|
415
|
+
return item;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Resize the Row Detail View */
|
|
419
|
+
resizeDetailView(item: any) {
|
|
420
|
+
if (!item) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Grad each of the DOM elements
|
|
425
|
+
const mainContainer = document.querySelector<HTMLDivElement>(`.${this.gridUid} .detailViewContainer_${item[this._dataViewIdProperty]}`);
|
|
426
|
+
const cellItem = document.querySelector<HTMLDivElement>(`.${this.gridUid} .cellDetailView_${item[this._dataViewIdProperty]}`);
|
|
427
|
+
const inner = document.querySelector<HTMLDivElement>(`.${this.gridUid} .innerDetailView_${item[this._dataViewIdProperty]}`);
|
|
428
|
+
|
|
429
|
+
if (!mainContainer || !cellItem || !inner) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
for (let idx = 1; idx <= item[`${this._keyPrefix}sizePadding`]; idx++) {
|
|
434
|
+
this.dataView.deleteItem(`${item[this._dataViewIdProperty]}.${idx}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const rowHeight = this.gridOptions.rowHeight as number; // height of a row
|
|
438
|
+
const lineHeight = 13; // we know cuz we wrote the custom css init ;)
|
|
439
|
+
|
|
440
|
+
// remove the height so we can calculate the height
|
|
441
|
+
mainContainer.style.minHeight = '';
|
|
442
|
+
|
|
443
|
+
// Get the scroll height for the main container so we know the actual size of the view
|
|
444
|
+
const itemHeight = mainContainer.scrollHeight;
|
|
445
|
+
|
|
446
|
+
// Now work out how many rows
|
|
447
|
+
const rowCount = Math.ceil(itemHeight / rowHeight);
|
|
448
|
+
|
|
449
|
+
item[`${this._keyPrefix}sizePadding`] = Math.ceil(((rowCount * 2) * lineHeight) / rowHeight);
|
|
450
|
+
item[`${this._keyPrefix}height`] = itemHeight;
|
|
451
|
+
|
|
452
|
+
let outterHeight = (item[`${this._keyPrefix}sizePadding`] * rowHeight);
|
|
453
|
+
if (this._addonOptions.maxRows !== undefined && item[`${this._keyPrefix}sizePadding`] > this._addonOptions.maxRows) {
|
|
454
|
+
outterHeight = this._addonOptions.maxRows! * rowHeight;
|
|
455
|
+
item[`${this._keyPrefix}sizePadding`] = this._addonOptions.maxRows;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// If the padding is now more than the original minRowBuff we need to increase it
|
|
459
|
+
if (this.gridOptions.minRowBuffer! < item[`${this._keyPrefix}sizePadding`]) {
|
|
460
|
+
// Update the minRowBuffer so that the view doesn't disappear when it's at top of screen + the original default 3
|
|
461
|
+
this.gridOptions.minRowBuffer = item[`${this._keyPrefix}sizePadding`] + 3;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
mainContainer.setAttribute('style', `min-height: ${item[this._keyPrefix + 'height']}px`);
|
|
465
|
+
if (cellItem) {
|
|
466
|
+
cellItem.setAttribute('style', `height: ${outterHeight}px; top: ${rowHeight}px`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const idxParent = this.dataView.getIdxById(item[this._dataViewIdProperty]) as number;
|
|
470
|
+
for (let idx = 1; idx <= item[`${this._keyPrefix}sizePadding`]; idx++) {
|
|
471
|
+
this.dataView.insertItem(idxParent + idx, this.getPaddingItem(item, idx));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Lastly save the updated state
|
|
475
|
+
this.saveDetailView(item);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// --
|
|
479
|
+
// protected functions
|
|
480
|
+
// ------------------
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* create the detail ctr node. this belongs to the dev & can be custom-styled as per
|
|
484
|
+
*/
|
|
485
|
+
protected applyTemplateNewLineHeight(item: any) {
|
|
486
|
+
// the height is calculated by the template row count (how many line of items does the template view have)
|
|
487
|
+
const rowCount = this._addonOptions.panelRows;
|
|
488
|
+
|
|
489
|
+
// calculate padding requirements based on detail-content..
|
|
490
|
+
// ie. worst-case: create an invisible dom node now & find it's height.
|
|
491
|
+
const lineHeight = 13; // we know cuz we wrote the custom css init ;)
|
|
492
|
+
item[`${this._keyPrefix}sizePadding`] = Math.ceil(((rowCount * 2) * lineHeight) / this.gridOptions.rowHeight!);
|
|
493
|
+
item[`${this._keyPrefix}height`] = (item[`${this._keyPrefix}sizePadding`] * this.gridOptions.rowHeight!);
|
|
494
|
+
const idxParent = this.dataView.getIdxById(item[this._dataViewIdProperty]);
|
|
495
|
+
for (let idx = 1; idx <= item[`${this._keyPrefix}sizePadding`]; idx++) {
|
|
496
|
+
this.dataView.insertItem((idxParent || 0) + idx, this.getPaddingItem(item, idx));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
protected calculateOutOfRangeViews() {
|
|
501
|
+
if (this._grid) {
|
|
502
|
+
let scrollDir: 'UP' | 'DOWN';
|
|
503
|
+
const renderedRange = this._grid.getRenderedRange();
|
|
504
|
+
// Only check if we have expanded rows
|
|
505
|
+
if (this._expandedRows.length > 0) {
|
|
506
|
+
// Assume scroll direction is down by default.
|
|
507
|
+
scrollDir = 'DOWN';
|
|
508
|
+
if (this._lastRange) {
|
|
509
|
+
// Some scrolling isn't anything as the range is the same
|
|
510
|
+
if (this._lastRange.top === renderedRange.top && this._lastRange.bottom === renderedRange.bottom) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// If our new top is smaller we are scrolling up
|
|
515
|
+
if (this._lastRange.top > renderedRange.top ||
|
|
516
|
+
// Or we are at very top but our bottom is increasing
|
|
517
|
+
(this._lastRange.top === 0 && renderedRange.top === 0 && (this._lastRange.bottom > renderedRange.bottom))) {
|
|
518
|
+
scrollDir = 'UP';
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
this._expandedRows.forEach((row) => {
|
|
524
|
+
const rowIndex = this.dataView.getRowById(row[this._dataViewIdProperty]) as number;
|
|
525
|
+
const rowPadding = row[`${this._keyPrefix}sizePadding`];
|
|
526
|
+
const isRowOutOfRange = this._rowIdsOutOfViewport.some(rowId => rowId === row[this._dataViewIdProperty]);
|
|
527
|
+
|
|
528
|
+
if (scrollDir === 'UP') {
|
|
529
|
+
// save the view when asked
|
|
530
|
+
if (this._addonOptions.saveDetailViewOnScroll) {
|
|
531
|
+
// If the bottom item within buffer range is an expanded row save it.
|
|
532
|
+
if (rowIndex >= renderedRange.bottom - this._gridRowBuffer) {
|
|
533
|
+
this.saveDetailView(row);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// If the row expanded area is within the buffer notify that it is back in range
|
|
538
|
+
if (isRowOutOfRange && ((rowIndex - this._outsideRange) < renderedRange.top) && (rowIndex >= renderedRange.top)) {
|
|
539
|
+
this.notifyBackToViewportWhenDomExist(row, row[this._dataViewIdProperty]);
|
|
540
|
+
} else if (!isRowOutOfRange && ((rowIndex + rowPadding) > renderedRange.bottom)) {
|
|
541
|
+
// if our first expanded row is about to go off the bottom
|
|
542
|
+
this.notifyOutOfViewport(row, row[this._dataViewIdProperty]);
|
|
543
|
+
}
|
|
544
|
+
} else if (scrollDir === 'DOWN') {
|
|
545
|
+
// save the view when asked
|
|
546
|
+
if (this._addonOptions.saveDetailViewOnScroll) {
|
|
547
|
+
// If the top item within buffer range is an expanded row save it.
|
|
548
|
+
if (rowIndex <= renderedRange.top + this._gridRowBuffer) {
|
|
549
|
+
this.saveDetailView(row);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// If row index is i higher than bottom with some added value (To ignore top rows off view) and is with view and was our of range
|
|
554
|
+
if (isRowOutOfRange && ((rowIndex + rowPadding + this._outsideRange) > renderedRange.bottom) && (rowIndex < (rowIndex + rowPadding))) {
|
|
555
|
+
this.notifyBackToViewportWhenDomExist(row, row[this._dataViewIdProperty]);
|
|
556
|
+
} else if (!isRowOutOfRange && (rowIndex < renderedRange.top)) {
|
|
557
|
+
// if our row is outside top of and the buffering zone but not in the array of outOfVisable range notify it
|
|
558
|
+
this.notifyOutOfViewport(row, row[this._dataViewIdProperty]);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
this._lastRange = renderedRange;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
protected calculateOutOfRangeViewsSimplerVersion() {
|
|
567
|
+
if (this._grid) {
|
|
568
|
+
const renderedRange = this._grid.getRenderedRange();
|
|
569
|
+
|
|
570
|
+
this._expandedRows.forEach((row) => {
|
|
571
|
+
const rowIndex = this.dataView.getRowById(row[this._dataViewIdProperty]) as number;
|
|
572
|
+
const isOutOfVisibility = this.checkIsRowOutOfViewportRange(rowIndex, renderedRange);
|
|
573
|
+
if (!isOutOfVisibility && this._rowIdsOutOfViewport.some(rowId => rowId === row[this._dataViewIdProperty])) {
|
|
574
|
+
this.notifyBackToViewportWhenDomExist(row, row[this._dataViewIdProperty]);
|
|
575
|
+
} else if (isOutOfVisibility) {
|
|
576
|
+
this.notifyOutOfViewport(row, row[this._dataViewIdProperty]);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
protected checkExpandableOverride(row: number, dataContext: any, grid: SlickGrid) {
|
|
583
|
+
if (typeof this._expandableOverride === 'function') {
|
|
584
|
+
return this._expandableOverride(row, dataContext, grid);
|
|
585
|
+
}
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
protected checkIsRowOutOfViewportRange(rowIndex: number, renderedRange: any) {
|
|
590
|
+
return (Math.abs(renderedRange.bottom - this._gridRowBuffer - rowIndex) > this._visibleRenderedCellCount * 2);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** Get the Row Detail padding (which are the rows dedicated to the detail panel) */
|
|
594
|
+
protected getPaddingItem(parent: any, offset: any) {
|
|
595
|
+
const item: any = {};
|
|
596
|
+
|
|
597
|
+
Object.keys(this.dataView).forEach(prop => {
|
|
598
|
+
if (prop) {
|
|
599
|
+
item[prop] = null;
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
item[this._dataViewIdProperty] = `${parent[this._dataViewIdProperty]}.${offset}`;
|
|
603
|
+
|
|
604
|
+
// additional hidden padding metadata fields
|
|
605
|
+
item[`${this._keyPrefix}collapsed`] = true;
|
|
606
|
+
item[`${this._keyPrefix}isPadding`] = true;
|
|
607
|
+
item[`${this._keyPrefix}parent`] = parent;
|
|
608
|
+
item[`${this._keyPrefix}offset`] = offset;
|
|
609
|
+
|
|
610
|
+
return item;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** The Formatter of the toggling icon of the Row Detail */
|
|
614
|
+
protected detailSelectionFormatter(row: number, _cell: number, _val: any, _colDef: Column, dataContext: any, grid: SlickGrid): FormatterResultWithHtml | HTMLElement | '' {
|
|
615
|
+
if (!this.checkExpandableOverride(row, dataContext, grid)) {
|
|
616
|
+
return '';
|
|
617
|
+
} else {
|
|
618
|
+
if (dataContext[`${this._keyPrefix}collapsed`] === undefined) {
|
|
619
|
+
dataContext[`${this._keyPrefix}collapsed`] = true;
|
|
620
|
+
dataContext[`${this._keyPrefix}sizePadding`] = 0; // the required number of pading rows
|
|
621
|
+
dataContext[`${this._keyPrefix}height`] = 0; // the actual height in pixels of the detail field
|
|
622
|
+
dataContext[`${this._keyPrefix}isPadding`] = false;
|
|
623
|
+
dataContext[`${this._keyPrefix}parent`] = undefined;
|
|
624
|
+
dataContext[`${this._keyPrefix}offset`] = 0;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (dataContext[`${this._keyPrefix}isPadding`]) {
|
|
628
|
+
// render nothing
|
|
629
|
+
} else if (dataContext[`${this._keyPrefix}collapsed`]) {
|
|
630
|
+
let collapsedClasses = `${this._addonOptions.cssClass || ''} expand `;
|
|
631
|
+
if (this._addonOptions.collapsedClass) {
|
|
632
|
+
collapsedClasses += this._addonOptions.collapsedClass;
|
|
633
|
+
}
|
|
634
|
+
return createDomElement('div', { className: classNameToList(collapsedClasses).join(' ') });
|
|
635
|
+
} else {
|
|
636
|
+
const rowHeight = this.gridOptions.rowHeight || 0;
|
|
637
|
+
let outterHeight = (dataContext[`${this._keyPrefix}sizePadding`] || 0) * this.gridOptions.rowHeight!;
|
|
638
|
+
|
|
639
|
+
if (this._addonOptions.maxRows !== null && ((dataContext[`${this._keyPrefix}sizePadding`] || 0) > this._addonOptions.maxRows!)) {
|
|
640
|
+
outterHeight = this._addonOptions.maxRows! * rowHeight!;
|
|
641
|
+
dataContext[`${this._keyPrefix}sizePadding`] = this._addonOptions.maxRows;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// sneaky extra </div> inserted here-----------------v
|
|
645
|
+
let expandedClasses = `${this._addonOptions.cssClass || ''} collapse `;
|
|
646
|
+
if (this._addonOptions.expandedClass) {
|
|
647
|
+
expandedClasses += this._addonOptions.expandedClass;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// create the Row Detail div container that will be inserted AFTER the `.slick-cell`
|
|
651
|
+
const cellDetailContainerElm = createDomElement('div', {
|
|
652
|
+
className: `dynamic-cell-detail cellDetailView_${dataContext[this._dataViewIdProperty]}`,
|
|
653
|
+
style: { height: `${outterHeight}px`, top: `${rowHeight}px` }
|
|
654
|
+
});
|
|
655
|
+
const innerContainerElm = createDomElement('div', { className: `detail-container detailViewContainer_${dataContext[this._dataViewIdProperty]}` });
|
|
656
|
+
const innerDetailViewElm = createDomElement('div', { className: `innerDetailView_${dataContext[this._dataViewIdProperty]}` });
|
|
657
|
+
if (dataContext[`${this._keyPrefix}detailContent`] instanceof HTMLElement) {
|
|
658
|
+
innerDetailViewElm.appendChild(dataContext[`${this._keyPrefix}detailContent`]);
|
|
659
|
+
} else {
|
|
660
|
+
innerDetailViewElm.innerHTML = this._grid.sanitizeHtmlString(dataContext[`${this._keyPrefix}detailContent`]);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
innerContainerElm.appendChild(innerDetailViewElm);
|
|
664
|
+
cellDetailContainerElm.appendChild(innerContainerElm);
|
|
665
|
+
|
|
666
|
+
const result: FormatterResultWithHtml = {
|
|
667
|
+
html: createDomElement('div', { className: classNameToList(expandedClasses).join(' ') }),
|
|
668
|
+
insertElementAfterTarget: cellDetailContainerElm,
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return '';
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** When row is getting toggled, we will handle the action of collapsing/expanding */
|
|
678
|
+
protected handleAccordionShowHide(item: any) {
|
|
679
|
+
if (item) {
|
|
680
|
+
if (!item[`${this._keyPrefix}collapsed`]) {
|
|
681
|
+
this.collapseDetailView(item);
|
|
682
|
+
} else {
|
|
683
|
+
this.expandDetailView(item);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** Handle mouse click event */
|
|
689
|
+
protected handleClick(e: SlickEventData, args: { row: number; cell: number; }) {
|
|
690
|
+
const dataContext = this._grid.getDataItem(args.row);
|
|
691
|
+
|
|
692
|
+
if (this.checkExpandableOverride(args.row, dataContext, this._grid)) {
|
|
693
|
+
// clicking on a row select checkbox
|
|
694
|
+
const columnDef = this._grid.getColumns()[args.cell];
|
|
695
|
+
if (this._addonOptions.useRowClick || (columnDef.id === this._addonOptions.columnId && e.target!.classList.contains(this._addonOptions.cssClass || ''))) {
|
|
696
|
+
// if editing, try to commit
|
|
697
|
+
if (this._grid.getEditorLock().isActive() && !this._grid.getEditorLock().commitCurrentEdit()) {
|
|
698
|
+
e.preventDefault();
|
|
699
|
+
e.stopImmediatePropagation();
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// trigger an event before toggling
|
|
704
|
+
// user could cancel the Row Detail opening when event is returning false
|
|
705
|
+
const ignorePrevEventDataValue = true; // click event might return false from Row Selection canCellBeActive() validation, we need to ignore that
|
|
706
|
+
if (this.onBeforeRowDetailToggle.notify({ grid: this._grid, item: dataContext }, e, this, ignorePrevEventDataValue).getReturnValue() === false) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
this.toggleRowSelection(args.row, dataContext);
|
|
711
|
+
|
|
712
|
+
// trigger an event after toggling
|
|
713
|
+
this.onAfterRowDetailToggle.notify({
|
|
714
|
+
grid: this._grid,
|
|
715
|
+
item: dataContext,
|
|
716
|
+
expandedRows: this._expandedRows,
|
|
717
|
+
}, e, this);
|
|
718
|
+
|
|
719
|
+
e.stopPropagation();
|
|
720
|
+
e.stopImmediatePropagation();
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
protected handleScroll() {
|
|
726
|
+
if (this._addonOptions.useSimpleViewportCalc) {
|
|
727
|
+
this.calculateOutOfRangeViewsSimplerVersion();
|
|
728
|
+
} else {
|
|
729
|
+
this.calculateOutOfRangeViews();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
protected notifyOutOfViewport(item: any, rowId: number | string) {
|
|
734
|
+
const rowIndex = item.rowIndex || this.dataView.getRowById(item[this._dataViewIdProperty]);
|
|
735
|
+
|
|
736
|
+
this.onRowOutOfViewportRange.notify({
|
|
737
|
+
grid: this._grid,
|
|
738
|
+
item,
|
|
739
|
+
rowId,
|
|
740
|
+
rowIndex,
|
|
741
|
+
expandedRows: this._expandedRows,
|
|
742
|
+
rowIdsOutOfViewport: this.syncOutOfViewportArray(rowId, true)
|
|
743
|
+
}, null, this);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
protected notifyBackToViewportWhenDomExist(item: any, rowId: number | string) {
|
|
747
|
+
const rowIndex = item.rowIndex || this.dataView.getRowById(item[this._dataViewIdProperty]);
|
|
748
|
+
|
|
749
|
+
setTimeout(() => {
|
|
750
|
+
// make sure View Row DOM Element really exist before notifying that it's a row that is visible again
|
|
751
|
+
if (document.querySelector(`.${this.gridUid} .cellDetailView_${item[this._dataViewIdProperty]}`)) {
|
|
752
|
+
this.onRowBackToViewportRange.notify({
|
|
753
|
+
grid: this._grid,
|
|
754
|
+
item,
|
|
755
|
+
rowId,
|
|
756
|
+
rowIndex,
|
|
757
|
+
expandedRows: this._expandedRows,
|
|
758
|
+
rowIdsOutOfViewport: this.syncOutOfViewportArray(rowId, false)
|
|
759
|
+
}, null, this);
|
|
760
|
+
}
|
|
761
|
+
}, 100);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
protected syncOutOfViewportArray(rowId: number | string, isAdding: boolean) {
|
|
765
|
+
const arrayRowIndex = this._rowIdsOutOfViewport.findIndex(outOfViewportRowId => outOfViewportRowId === rowId);
|
|
766
|
+
|
|
767
|
+
if (isAdding && arrayRowIndex < 0) {
|
|
768
|
+
this._rowIdsOutOfViewport.push(rowId);
|
|
769
|
+
} else if (!isAdding && arrayRowIndex >= 0) {
|
|
770
|
+
this._rowIdsOutOfViewport.splice(arrayRowIndex, 1);
|
|
771
|
+
}
|
|
772
|
+
return this._rowIdsOutOfViewport;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
protected toggleRowSelection(rowNumber: number, dataContext: any) {
|
|
776
|
+
if (this.checkExpandableOverride(rowNumber, dataContext, this._grid)) {
|
|
777
|
+
this.dataView.beginUpdate();
|
|
778
|
+
this.handleAccordionShowHide(dataContext);
|
|
779
|
+
this.dataView.endUpdate();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|