Skip to content

Commit

Permalink
Merge pull request #384 from creamarketing/nested-gridfield
Browse files Browse the repository at this point in the history
NEW Nested gridfield
  • Loading branch information
GuySartorelli authored May 15, 2024
2 parents 256d06b + a42cd43 commit 678ec6f
Show file tree
Hide file tree
Showing 10 changed files with 1,046 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ This module provides a number of useful grid field components:
features.
* `GridFieldTitleHeader` - a simple header which displays column titles.
* `GridFieldConfigurablePaginator` - a paginator for GridField that allows customisable page sizes.
* `GridFieldNestedForm` - allows nesting of GridFields for managing relation records directly within
a parent GridField.

## Installation

Expand Down
24 changes: 24 additions & 0 deletions css/GridFieldExtensions.css
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,27 @@
.grid-field-inline-new--multi-class-list__visible {
display: block;
}

/**
* GridFieldNestedForm
*/
.grid-field tr.nested-gridfield td.gridfield-holder {
padding-left: 60px;
}

.grid-field.nested.empty-title .grid-field__title-row th {
padding: 0;
}

.grid-field.nested table tbody tr:not(.nested-gridfield) {
border-left: 1px solid #dbe0e9;
}

.grid-field.nested table tbody tr:not(.nested-gridfield).last {
border-bottom: 1px solid #dbe0e9;
}

.ss-gridfield-orderable.has-nested > .grid-field__table > .ss-gridfield-items > .ss-gridfield-item.ui-droppable-active.ui-state-highlight {
border: 0;
background-color: #fbf9ee;
}
43 changes: 43 additions & 0 deletions docs/en/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,46 @@ $paginator->setItemsPerPage(500);

The first shown record will be maintained across page size changes, and the number of pages and current page will be
recalculated on each request, based on the current first shown record and page size.

Nested GridFields
-----------------

The `GridFieldNestedForm` component allows you to nest GridFields in the UI. It can be used with `DataObject` subclasses
with the `Hierarchy` extension, or by specifying the relation used for nesting.

```php
// Basic usage, defaults to the Children-method for Hierarchy objects.
$grid->getConfig()->addComponent(GridFieldNestedForm::create());

// Usage with custom relation
$grid->getConfig()->addComponent(GridFieldNestedForm::create()->setRelationName('MyRelation'));
```

You can define your own custom GridField config for the nested GridField configuration by implementing a `getNestedConfig`
on your nested model (should return a `GridField_Config` object).
```php
class NestedObject extends DataObject
{
private static $has_one = [
'Parent' => ParentObject::class
];

public function getNestedConfig(): GridFieldConfig
{
$config = new GridFieldConfig_RecordViewer();
return $config;
}
}
```

You can also modify the default config (a `GridFieldConfig_RecordEditor`) via an extension to the nested model class, by implementing
`updateNestedConfig`, which will get the config object as the first parameter.
```php
class NestedObjectExtension extends DataExtension
{
public function updateNestedConfig(GridFieldConfig &$config)
{
$config->removeComponentsByType(GridFieldPaginator::class);
}
}
```
171 changes: 171 additions & 0 deletions javascript/GridFieldExtensions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
(function($) {
let preventReorderUpdate = false;
let updateTimeouts = [];

$.entwine("ss", function($) {
/**
* GridFieldAddExistingSearchButton
Expand Down Expand Up @@ -510,5 +513,173 @@
this.parent().find('.ss-gridfield-pagesize-submit').trigger('click');
}
});

/**
* GridFieldNestedForm
*/
$('.grid-field .col-listChildrenLink button').entwine({
onclick: function(e) {
let gridField = $(this).closest('.grid-field');
let currState = gridField.getState();
let toggleState = false;
let pjaxTarget = $(this).attr('data-pjax-target');
if ($(this).hasClass('font-icon-right-dir')) {
toggleState = true;
}
if (typeof currState['GridFieldNestedForm'] == 'undefined' || currState['GridFieldNestedForm'] == null) {
currState['GridFieldNestedForm'] = {};
}
currState['GridFieldNestedForm'][$(this).attr('data-pjax-target')] = toggleState;
gridField.setState('GridFieldNestedForm', currState['GridFieldNestedForm']);
if (toggleState) {
if (!$(this).closest('tr').next('.nested-gridfield').length) {
// add loading indicator until the nested gridfield is loaded
let colspan = gridField.find('.grid-field__title-row th').attr('colspan');
let loadingCell = $('<td />')
.addClass('ss-gridfield-item loading')
.attr('colspan', colspan);
$(this).closest('tr').after($('<tr class="nested-gridfield" />').append(loadingCell));

let data = {};
let stateInput = gridField.find('input.gridstate').first();
data[stateInput.attr('name')] = JSON.stringify(currState);
if (window.location.search) {
let searchParams = window.location.search.replace('?', '').split('&');
for (let i = 0; i < searchParams.length; i++) {
let parts = searchParams[i].split('=');
data[parts[0]] = parts[1];
}
}
$.ajax({
type: 'POST',
url: $(this).attr('data-url'),
data: data,
headers: {
'X-Pjax': pjaxTarget
},
success: function(data) {
if (data && data[pjaxTarget]) {
gridField.find(`[data-pjax-fragment="${pjaxTarget}"]`).replaceWith(data[pjaxTarget]);
}
}
});
}
else {
$(this).closest('tr').next('.nested-gridfield').show();
$.ajax({
url: $(this).attr('data-toggle')+'1'
});
}
$(this).removeClass('font-icon-right-dir');
$(this).addClass('font-icon-down-dir');
$(this).attr('aria-expanded', 'true');
}
else {
$.ajax({
url: $(this).attr('data-toggle')+'0'
});
$(this).closest('tr').next('.nested-gridfield').hide();
$(this).removeClass('font-icon-down-dir');
$(this).addClass('font-icon-right-dir');
$(this).attr('aria-expanded', 'false');
}
e.preventDefault();
e.stopPropagation();
return false;
}
});

// move nested gridfields onto their own rows below this row, to make it look nicer
$('.col-listChildrenLink > .grid-field.nested').entwine({
onadd: function() {
let nrOfColumns = $(this).closest('tr').children('td').length;
let evenOrOdd = 'even';
if ($(this).closest('tr').hasClass('odd')) {
evenOrOdd = 'odd';
}
if ($(this).closest('.grid-field').hasClass('editable-gridfield')) {
$(this).find('tr').removeClass('even').removeClass('odd').addClass(evenOrOdd);
}

if ($(this).closest('tr').next('tr.nested-gridfield').length) {
$(this).closest('tr').next('tr.nested-gridfield').remove();
}

// add a new table row, with one table cell which spans all columns
$(this).closest('tr').after('<tr class="nested-gridfield '+evenOrOdd+'"><td class="gridfield-holder" colspan="'+nrOfColumns+'"></td></tr>');
// move this field into the newly created row
$(this).appendTo($(this).closest('tr').next('tr').find('td').first());
$(this).show();
this._super();
}
});

$('.ss-gridfield-orderable.has-nested > .grid-field__table > tbody, .ss-gridfield-orderable.nested > .grid-field__table > tbody').entwine({
onadd: function() {
this._super();
let gridField = this.getGridField();
if (gridField.data("url-movetoparent")) {
let parentID = 0;
let parentItem = gridField.closest('.nested-gridfield').prev('.ss-gridfield-item');
if (parentItem && parentItem.length) {
parentID = parentItem.attr('data-id');
}
this.sortable('option', 'connectWith', '.ss-gridfield-orderable tbody');
this.sortable('option', 'start', function(e, ui) {
if (ui.item.find('.col-listChildrenLink').length && ui.item.next('.ui-sortable-placeholder').next('.nested-gridfield').length) {
if (ui.item.find('.col-listChildrenLink a').hasClass('font-icon-down-dir')) {
ui.item.find('.col-listChildrenLink a').removeClass('font-icon-down-dir');
ui.item.find('.col-listChildrenLink a').addClass('font-icon-right-dir');
}
ui.item.next('.ui-sortable-placeholder').next('.nested-gridfield').remove();
let pjaxFragment = ui.item.find('.col-listChildrenLink a').attr('data-pjax-target');
ui.item.find('.col-listChildrenLink').append(`<div class="nested-container" data-pjax-fragment="${pjaxFragment}" style="display:none;"></div>`);
}
});
this.sortable('option', 'receive', function(e, ui) {
preventReorderUpdate = true;
while (updateTimeouts.length) {
let timeout = updateTimeouts.shift();
window.clearTimeout(timeout);
}
let childID = ui.item.attr('data-id');
let parentIntoChild = $(e.target).closest('.grid-field[data-name*="-GridFieldNestedForm-'+childID+'"]').length;
if (parentIntoChild) {
// parent dragged into child, cancel sorting
ui.sender.sortable("cancel");
e.preventDefault();
e.stopPropagation();
window.setTimeout(function() {
preventReorderUpdate = false;
}, 500);
return false;
}
let sortInput = ui.item.find('input.ss-orderable-hidden-sort');
let sortName = sortInput.attr('name');
let index = sortName.indexOf('[GridFieldEditableColumns]');
sortInput.attr('name', gridField.attr('data-name')+sortName.substring(index));
gridField.find('> .grid-field__table > tbody').rebuildSort();
gridField.reload({
url: gridField.data("url-movetoparent"),
data: [
{ name: "move[id]", value: childID},
{ name: "move[parent]", value: parentID}
]
}, function() {
preventReorderUpdate = false;
});
});
let updateCallback = this.sortable('option', 'update');
this.sortable('option', 'update', function(e, ui) {
if (!preventReorderUpdate) {
let timeout = window.setTimeout(function() {
updateCallback(e, ui);
}, 500);
updateTimeouts.push(timeout);
}
});
}
}
});
});
})(jQuery);
Loading

0 comments on commit 678ec6f

Please sign in to comment.