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