UNPKG

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