Passed
Push — master ( 337499...e206d5 )
by Thomas
12:05
created

TabulatorGrid::getGroupLayout()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
namespace LeKoala\Tabulator;
4
5
use Exception;
6
use RuntimeException;
7
use SilverStripe\i18n\i18n;
8
use SilverStripe\Forms\Form;
9
use InvalidArgumentException;
10
use SilverStripe\ORM\SS_List;
11
use SilverStripe\Core\Convert;
12
use SilverStripe\ORM\DataList;
13
use SilverStripe\ORM\ArrayList;
14
use SilverStripe\Core\ClassInfo;
15
use SilverStripe\ORM\DataObject;
16
use SilverStripe\View\ArrayData;
17
use SilverStripe\Forms\FieldList;
18
use SilverStripe\Forms\FormField;
19
use SilverStripe\Control\Director;
20
use SilverStripe\View\Requirements;
21
use SilverStripe\Control\Controller;
22
use SilverStripe\Control\HTTPRequest;
23
use SilverStripe\Control\HTTPResponse;
24
use SilverStripe\ORM\FieldType\DBEnum;
25
use SilverStripe\Control\RequestHandler;
26
use SilverStripe\Core\Injector\Injector;
27
use SilverStripe\Security\SecurityToken;
28
use SilverStripe\ORM\DataObjectInterface;
29
use SilverStripe\ORM\FieldType\DBBoolean;
30
use LeKoala\ModularBehaviour\ModularFormField;
31
use SilverStripe\Forms\GridField\GridFieldConfig;
32
use SilverStripe\Core\Manifest\ModuleResourceLoader;
33
34
/**
35
 * This is a replacement for most GridField usages in SilverStripe
36
 * It can easily work in the frontend too
37
 *
38
 * @link http://www.tabulator.info/
39
 */
40
class TabulatorGrid extends ModularFormField
41
{
42
    const POS_START = 'start';
43
    const POS_END = 'end';
44
45
    // @link http://www.tabulator.info/examples/5.4?#fittodata
46
    const LAYOUT_FIT_DATA = "fitData";
47
    const LAYOUT_FIT_DATA_FILL = "fitDataFill";
48
    const LAYOUT_FIT_DATA_STRETCH = "fitDataStretch";
49
    const LAYOUT_FIT_DATA_TABLE = "fitDataTable";
50
    const LAYOUT_FIT_COLUMNS = "fitColumns";
51
52
    const RESPONSIVE_LAYOUT_HIDE = "hide";
53
    const RESPONSIVE_LAYOUT_COLLAPSE = "collapse";
54
55
    // @link http://www.tabulator.info/docs/5.4/format
56
    const FORMATTER_PLAINTEXT = 'plaintext';
57
    const FORMATTER_TEXTAREA = 'textarea';
58
    const FORMATTER_HTML = 'html';
59
    const FORMATTER_MONEY = 'money';
60
    const FORMATTER_IMAGE = 'image';
61
    const FORMATTER_LINK = 'link';
62
    const FORMATTER_DATETIME = 'datetime';
63
    const FORMATTER_DATETIME_DIFF = 'datetimediff';
64
    const FORMATTER_TICKCROSS = 'tickCross';
65
    const FORMATTER_COLOR = 'color';
66
    const FORMATTER_STAR = 'star';
67
    const FORMATTER_TRAFFIC = 'traffic';
68
    const FORMATTER_PROGRESS = 'progress';
69
    const FORMATTER_LOOKUP = 'lookup';
70
    const FORMATTER_BUTTON_TICK = 'buttonTick';
71
    const FORMATTER_BUTTON_CROSS = 'buttonCross';
72
    const FORMATTER_ROWNUM = 'rownum';
73
    const FORMATTER_HANDLE = 'handle';
74
    // @link http://www.tabulator.info/docs/5.4/format#format-module
75
    const FORMATTER_ROW_SELECTION = 'rowSelection';
76
    const FORMATTER_RESPONSIVE_COLLAPSE = 'responsiveCollapse';
77
78
    // our built in functions
79
    const JS_FLAG_FORMATTER = 'SSTabulator.flagFormatter';
80
    const JS_BUTTON_FORMATTER = 'SSTabulator.buttonFormatter';
81
    const JS_CUSTOM_TICK_CROSS_FORMATTER = 'SSTabulator.customTickCrossFormatter';
82
    const JS_BOOL_GROUP_HEADER = 'SSTabulator.boolGroupHeader';
83
    const JS_SIMPLE_ROW_FORMATTER = 'SSTabulator.simpleRowFormatter';
84
    const JS_EXPAND_TOOLTIP = 'SSTabulator.expandTooltip';
85
    const JS_DATA_AJAX_RESPONSE = 'SSTabulator.dataAjaxResponse';
86
87
    /**
88
     * @config
89
     */
90
    private static array $allowed_actions = [
91
        'load',
92
        'handleItem',
93
        'handleTool',
94
        'configProvider',
95
        'autocomplete',
96
        'handleBulkAction',
97
    ];
98
99
    private static $url_handlers = [
100
        'item/$ID' => 'handleItem',
101
        'tool/$ID' => 'handleTool',
102
        'bulkAction/$ID' => 'handleBulkAction',
103
    ];
104
105
    private static array $casting = [
106
        'JsonOptions' => 'HTMLFragment',
107
        'ShowTools' => 'HTMLFragment',
108
        'dataAttributesHTML' => 'HTMLFragment',
109
    ];
110
111
    /**
112
     * @config
113
     */
114
    private static string $theme = 'bootstrap5';
115
116
    /**
117
     * @config
118
     */
119
    private static string $version = '5.4';
120
121
    /**
122
     * @config
123
     */
124
    private static string $luxon_version = '3';
125
126
    /**
127
     * @config
128
     */
129
    private static string $last_icon_version = '2';
130
131
    /**
132
     * @config
133
     */
134
    private static bool $use_cdn = false;
135
136
    /**
137
     * @config
138
     */
139
    private static bool $use_custom_build = true;
140
141
    /**
142
     * @config
143
     */
144
    private static bool $enable_luxon = false;
145
146
    /**
147
     * @config
148
     */
149
    private static bool $enable_last_icon = false;
150
151
    /**
152
     * @config
153
     */
154
    private static bool $enable_requirements = true;
155
156
    /**
157
     * @config
158
     */
159
    private static bool $enable_js_modules = true;
160
161
    /**
162
     * @link http://www.tabulator.info/docs/5.4/options
163
     * @config
164
     */
165
    private static array $default_options = [
166
        'index' => "ID", // http://tabulator.info/docs/5.4/data#row-index
167
        'layout' => 'fitColumns', // http://www.tabulator.info/docs/5.4/layout#layout
168
        'height' => '100%', // http://www.tabulator.info/docs/5.4/layout#height-fixed
169
        'responsiveLayout' => "hide", // http://www.tabulator.info/docs/5.4/layout#responsive
170
        'rowFormatter' => "SSTabulator.simpleRowFormatter", // http://tabulator.info/docs/5.4/format#row
171
    ];
172
173
    /**
174
     * @link http://tabulator.info/docs/5.4/columns#defaults
175
     * @config
176
     */
177
    private static array $default_column_options = [
178
        'resizable' => false,
179
        'tooltip' => 'SSTabulator.expandTooltip',
180
    ];
181
182
    private static bool $enable_ajax_init = true;
183
184
    /**
185
     * @config
186
     */
187
    private static array $custom_pagination_icons = [];
188
189
    /**
190
     * @config
191
     */
192
    private static bool $default_lazy_init = false;
193
194
    /**
195
     * Data source.
196
     */
197
    protected ?SS_List $list;
198
199
    /**
200
     * @link http://www.tabulator.info/docs/5.4/columns
201
     */
202
    protected array $columns = [];
203
204
    /**
205
     * @link http://tabulator.info/docs/5.4/columns#defaults
206
     */
207
    protected array $columnDefaults = [];
208
209
    /**
210
     * @link http://www.tabulator.info/docs/5.4/options
211
     */
212
    protected array $options = [];
213
214
    protected bool $autoloadDataList = true;
215
216
    protected bool $rowClickTriggersAction = false;
217
218
    protected int $pageSize = 10;
219
220
    protected string $itemRequestClass = '';
221
222
    protected string $modelClass = '';
223
224
    protected bool $lazyInit = false;
225
226
    protected array $tools = [];
227
228
    /**
229
     * @var AbstractBulkAction[]
230
     */
231
    protected array $bulkActions = [];
232
233
    protected array $listeners = [];
234
235
    protected array $jsNamespaces = [
236
        'SSTabulator'
237
    ];
238
239
    protected array $linksOptions = [
240
        'ajaxURL'
241
    ];
242
243
    protected array $dataAttributes = [];
244
245
    protected string $controllerFunction = "";
246
247
    protected bool $useConfigProvider = false;
248
249
    protected bool $useInitScript = false;
250
251
    protected string $editUrl = "";
252
253
    protected string $moveUrl = "";
254
255
    protected string $bulkUrl = "";
256
257
    protected bool $globalSearch = false;
258
259
    protected array $wildcardFields = [];
260
261
    protected array $quickFilters = [];
262
263
    protected bool $groupLayout = false;
264
265
    protected bool $enableGridManipulation = false;
266
267
    /**
268
     * @param string $fieldName
269
     * @param string|null|bool $title
270
     * @param SS_List $value
271
     */
272
    public function __construct($name, $title = null, $value = null)
273
    {
274
        parent::__construct($name, $title, $value);
275
        $this->options = self::config()->default_options ?? [];
276
        $this->columnDefaults = self::config()->default_column_options ?? [];
277
        $this->setLazyInit(self::config()->default_lazy_init);
278
279
        // We don't want regular setValue for this since it would break with loadFrom logic
280
        if ($value) {
281
            $this->setList($value);
282
        }
283
    }
284
285
    /**
286
     * This helps if some third party code expects the TabulatorGrid to be a GridField
287
     * Only works to a really basic extent
288
     */
289
    public function getConfig(): GridFieldConfig
290
    {
291
        return new GridFieldConfig;
292
    }
293
294
    /**
295
     * This helps if some third party code expects the TabulatorGrid to be a GridField
296
     * Only works to a really basic extent
297
     */
298
    public function setConfig($config)
299
    {
300
        // ignore
301
    }
302
303
    /**
304
     * @return string
305
     */
306
    public function getValueJson()
307
    {
308
        $v = $this->value ?? '';
309
        if (is_array($v)) {
310
            $v = json_encode($v);
311
        }
312
        if (strpos($v, '[') !== 0) {
313
            return '[]';
314
        }
315
        return $v;
316
    }
317
318
    public function saveInto(DataObjectInterface $record)
319
    {
320
        if ($this->enableGridManipulation) {
321
            $value = $this->dataValue();
322
            if (is_array($value)) {
323
                $this->value = json_encode(array_values($value));
324
            }
325
            parent::saveInto($record);
326
        }
327
    }
328
329
    /**
330
     * Temporary link that will be replaced by a real link by processLinks
331
     * TODO: not really happy with this, find a better way
332
     *
333
     * @param string $action
334
     * @return string
335
     */
336
    public function TempLink(string $action, bool $controller = true): string
337
    {
338
        // It's an absolute link
339
        if (strpos($action, '/') === 0 || strpos($action, 'http') === 0) {
340
            return $action;
341
        }
342
        // Already temp
343
        if (strpos($action, ':') !== false) {
344
            return $action;
345
        }
346
        $prefix = $controller ? "controller" : "form";
347
        return "$prefix:$action";
348
    }
349
350
    public function ControllerLink(string $action): string
351
    {
352
        return $this->getForm()->getController()->Link($action);
353
    }
354
355
    public function getCreateLink(): string
356
    {
357
        return Controller::join_links($this->Link('item'), 'new');
358
    }
359
360
    /**
361
     * @param FieldList $fields
362
     * @param string $name
363
     * @return TabulatorGrid|null
364
     */
365
    public static function replaceGridField(FieldList $fields, string $name)
366
    {
367
        /** @var \SilverStripe\Forms\GridField\GridField $gridField */
368
        $gridField = $fields->dataFieldByName($name);
369
        if (!$gridField) {
0 ignored issues
show
introduced by
$gridField is of type SilverStripe\Forms\GridField\GridField, thus it always evaluated to true.
Loading history...
370
            return;
371
        }
372
        if ($gridField instanceof TabulatorGrid) {
0 ignored issues
show
introduced by
$gridField is never a sub-type of LeKoala\Tabulator\TabulatorGrid.
Loading history...
373
            return $gridField;
374
        }
375
        $tabulatorGrid = new TabulatorGrid($name, $gridField->Title(), $gridField->getList());
376
        // In the cms, this is mostly never happening
377
        if ($gridField->getForm()) {
378
            $tabulatorGrid->setForm($gridField->getForm());
379
        }
380
        $tabulatorGrid->configureFromDataObject($gridField->getModelClass());
381
        $tabulatorGrid->setLazyInit(true);
382
        $fields->replaceField($name, $tabulatorGrid);
383
384
        return $tabulatorGrid;
385
    }
386
387
    public function configureFromDataObject($className = null, bool $clear = true): void
388
    {
389
        $this->columns = [];
390
391
        if (!$className) {
392
            $className = $this->getModelClass();
393
        }
394
        if (!$className) {
395
            throw new RuntimeException("Could not find the model class");
396
        }
397
        $this->modelClass = $className;
398
399
        /** @var DataObject $singl */
400
        $singl = singleton($className);
401
402
        // Mock some base columns using SilverStripe built-in methods
403
        $columns = [];
404
405
        foreach ($singl->summaryFields() as $field => $title) {
406
            // Deal with this in load() instead
407
            // if (strpos($field, '.') !== false) {
408
            // $fieldParts = explode(".", $field);
409
410
            // It can be a relation Users.Count or a field Field.Nice
411
            // $classOrField = $fieldParts[0];
412
            // $relationOrMethod = $fieldParts[1];
413
            // }
414
            $title = str_replace(".", " ", $title);
415
            $columns[$field] = [
416
                'field' => $field,
417
                'title' => $title,
418
            ];
419
420
            $dbObject = $singl->dbObject($field);
421
            if ($dbObject) {
422
                if ($dbObject instanceof DBBoolean) {
423
                    $columns[$field]['formatter'] = "SSTabulator.customTickCrossFormatter";
424
                }
425
            }
426
        }
427
        foreach ($singl->searchableFields() as $key => $searchOptions) {
428
            /*
429
            "filter" => "NameOfTheFilter"
430
            "field" => "SilverStripe\Forms\FormField"
431
            "title" => "Title of the field"
432
            */
433
            if (!isset($columns[$key])) {
434
                continue;
435
            }
436
            $columns[$key]['headerFilter'] = true;
437
            // $columns[$key]['headerFilterPlaceholder'] = $searchOptions['title'];
438
            //TODO: implement filter mapping
439
            switch ($searchOptions['filter']) {
440
                default:
441
                    $columns[$key]['headerFilterFunc'] =  "like";
442
                    break;
443
            }
444
445
            // Restrict based on data type
446
            $dbObject = $singl->dbObject($key);
447
            if ($dbObject) {
448
                if ($dbObject instanceof DBBoolean) {
449
                    $columns[$key]['headerFilter'] = 'tickCross';
450
                    $columns[$key]['headerFilterFunc'] =  "=";
451
                    $columns[$key]['headerFilterParams'] =  [
452
                        'tristate' => true
453
                    ];
454
                }
455
                if ($dbObject instanceof DBEnum) {
456
                    $columns[$key]['headerFilter'] = 'list';
457
                    $columns[$key]['headerFilterFunc'] =  "=";
458
                    $columns[$key]['headerFilterParams'] =  [
459
                        'values' => $dbObject->enumValues()
460
                    ];
461
                }
462
            }
463
        }
464
465
        // Allow customizing our columns based on record
466
        if ($singl->hasMethod('tabulatorColumns')) {
467
            $fields = $singl->tabulatorColumns();
468
            if (!is_array($fields)) {
469
                throw new RuntimeException("tabulatorColumns must return an array");
470
            }
471
            foreach ($fields as $key => $columnOptions) {
472
                $baseOptions = $columns[$key] ?? [];
473
                $columns[$key] = array_merge($baseOptions, $columnOptions);
474
            }
475
        }
476
477
        $this->extend('updateConfiguredColumns', $columns);
478
479
        foreach ($columns as $col) {
480
            $this->addColumn($col['field'], $col['title'], $col);
481
        }
482
483
        // Sortable ?
484
        if ($singl->hasField('Sort')) {
485
            $this->wizardMoveable();
486
        }
487
488
        // Actions
489
        // We use a pseudo link, because maybe we cannot call Link() yet if it's not linked to a form
490
491
        $this->bulkUrl = $this->TempLink("bulkAction/", false);
492
493
        // - Core actions, handled by TabulatorGrid
494
        $itemUrl = $this->TempLink('item/{ID}', false);
495
        if ($singl->canEdit()) {
496
            $this->addButton("ui_edit", $itemUrl, "edit", "Edit");
497
            $this->editUrl = $this->TempLink("item/{ID}/ajaxEdit", false);
498
        } elseif ($singl->canView()) {
499
            $this->addButton("ui_view", $itemUrl, "visibility", "View");
500
        }
501
502
        // - Tools
503
        $this->tools = [];
504
        if ($singl->canCreate()) {
505
            $this->addTool(self::POS_START, new TabulatorAddNewButton($this), 'add_new');
0 ignored issues
show
Unused Code introduced by
The call to LeKoala\Tabulator\Tabula...ewButton::__construct() has too many arguments starting with $this. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

505
            $this->addTool(self::POS_START, /** @scrutinizer ignore-call */ new TabulatorAddNewButton($this), 'add_new');

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
506
        }
507
        if (class_exists(\LeKoala\ExcelImportExport\ExcelImportExport::class)) {
0 ignored issues
show
Bug introduced by
The type LeKoala\ExcelImportExport\ExcelImportExport was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
508
            $this->addTool(self::POS_END, new TabulatorExportButton($this), 'export');
0 ignored issues
show
Unused Code introduced by
The call to LeKoala\Tabulator\Tabula...rtButton::__construct() has too many arguments starting with $this. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

508
            $this->addTool(self::POS_END, /** @scrutinizer ignore-call */ new TabulatorExportButton($this), 'export');

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
509
        }
510
511
        // - Custom actions are forwarded to the model itself
512
        if ($singl->hasMethod('tabulatorRowActions')) {
513
            $rowActions = $singl->tabulatorRowActions();
514
            if (!is_array($rowActions)) {
515
                throw new RuntimeException("tabulatorRowActions must return an array");
516
            }
517
            foreach ($rowActions as $key => $actionConfig) {
518
                $action = $actionConfig['action'] ?? $key;
519
                $url = $this->TempLink("item/{ID}/customAction/$action", false);
520
                $icon = $actionConfig['icon'] ?? "cog";
521
                $title = $actionConfig['title'] ?? "";
522
523
                $button = $this->makeButton($url, $icon, $title);
524
                if (!empty($actionConfig['ajax'])) {
525
                    $button['formatterParams']['ajax'] = true;
526
                }
527
                $this->addButtonFromArray("ui_customaction_$action", $button);
528
            }
529
        }
530
531
        $this->setRowClickTriggersAction(true);
532
    }
533
534
    public static function requirements(): void
535
    {
536
        $use_cdn = self::config()->use_cdn;
537
        $use_custom_build = self::config()->use_custom_build;
538
        $theme = self::config()->theme; // simple, midnight, modern or framework
539
        $version = self::config()->version;
540
        $luxon_version = self::config()->luxon_version;
541
        $enable_luxon = self::config()->enable_luxon;
542
        $last_icon_version = self::config()->last_icon_version;
543
        $enable_last_icon = self::config()->enable_last_icon;
544
        $enable_js_modules = self::config()->enable_js_modules;
545
546
        $jsOpts = [];
547
        if ($enable_js_modules) {
548
            $jsOpts['type'] = 'module';
549
        }
550
551
        if ($use_cdn) {
552
            $baseDir = "https://cdn.jsdelivr.net/npm/tabulator-tables@$version/dist";
553
        } else {
554
            $asset = ModuleResourceLoader::resourceURL('lekoala/silverstripe-tabulator:client/cdn/js/tabulator.min.js');
555
            $baseDir = dirname(dirname($asset));
556
        }
557
558
        if ($luxon_version && $enable_luxon) {
559
            // Do not load as module or we would get undefined luxon global var
560
            Requirements::javascript("https://cdn.jsdelivr.net/npm/luxon@$luxon_version/build/global/luxon.min.js");
561
        }
562
        if ($last_icon_version && $enable_last_icon) {
563
            Requirements::css("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.css");
564
            // Do not load as module even if asked to ensure load speed
565
            Requirements::javascript("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.js");
566
        }
567
        if ($use_custom_build) {
568
            // if (Director::isDev() && !Director::is_ajax()) {
569
            //     Requirements::javascript("lekoala/silverstripe-tabulator:client/custom-tabulator.js");
570
            // } else {
571
            Requirements::javascript("lekoala/silverstripe-tabulator:client/custom-tabulator.min.js", $jsOpts);
572
            // }
573
            Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js', $jsOpts);
574
        } else {
575
            Requirements::javascript("$baseDir/js/tabulator.min.js", $jsOpts);
576
            Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js', $jsOpts);
577
        }
578
579
        if ($theme) {
580
            Requirements::css("$baseDir/css/tabulator_$theme.min.css");
581
        } else {
582
            Requirements::css("$baseDir/css/tabulator.min.css");
583
        }
584
585
        if ($theme && $theme == "bootstrap5") {
586
            Requirements::css('lekoala/silverstripe-tabulator:client/custom-tabulator.min.css');
587
        }
588
    }
589
590
    public function setValue($value, $data = null)
591
    {
592
        // Allow set raw json as value
593
        if ($value && is_string($value) && strpos($value, '[') === 0) {
594
            $value = json_decode($value);
595
        }
596
        if ($value instanceof DataList) {
597
            $this->configureFromDataObject($value->dataClass());
598
        }
599
        return parent::setValue($value, $data);
600
    }
601
602
    public function getModularName()
603
    {
604
        return 'SSTabulator.createTabulator';
605
    }
606
607
    public function getModularSelector()
608
    {
609
        return '.tabulatorgrid';
610
    }
611
612
    public function getModularConfigName()
613
    {
614
        return str_replace('-', '_', $this->ID()) . '_config';
615
    }
616
617
    public function getModularConfig()
618
    {
619
        $JsonOptions = $this->JsonOptions();
620
        $configName = $this->getModularConfigName();
621
        $script = "var $configName = $JsonOptions";
622
        return $script;
623
    }
624
625
    public function Field($properties = [])
626
    {
627
        $this->addExtraClass(self::config()->theme);
628
        if ($this->lazyInit) {
629
            $this->setModularLazy($this->lazyInit);
630
        }
631
        if (self::config()->enable_requirements) {
632
            self::requirements();
633
        }
634
635
        // Make sure we can use a standalone version of the field without a form
636
        // Function should match the name
637
        if (!$this->form) {
638
            $this->form = new Form(Controller::curr(), $this->getControllerFunction());
639
        }
640
641
        // Data attributes for our custom behaviour
642
        $this->setDataAttribute("row-click-triggers-action", $this->rowClickTriggersAction);
643
        $customIcons = self::config()->custom_pagination_icons;
644
        $this->setDataAttribute("use-custom-pagination-icons", empty($customIcons));
645
646
        $this->setDataAttribute("listeners", $this->listeners);
647
        if ($this->editUrl) {
648
            $url = $this->processLink($this->editUrl);
649
            $this->setDataAttribute("edit-url", $url);
650
        }
651
        if ($this->moveUrl) {
652
            $url = $this->processLink($this->moveUrl);
653
            $this->setDataAttribute("move-url", $url);
654
        }
655
        if (!empty($this->bulkActions)) {
656
            $url = $this->processLink($this->bulkUrl);
657
            $this->setDataAttribute("bulk-url", $url);
658
        }
659
660
        if ($this->useConfigProvider) {
661
            $configLink = "/" . ltrim($this->Link("configProvider"), "/");
662
            $configLink .= "?t=" . time();
663
            // This cannot be loaded as a js module
664
            Requirements::javascript($configLink, ['type' => 'application/javascript', 'defer' => 'true']);
665
        } elseif ($this->useInitScript) {
666
            Requirements::customScript($this->getInitScript());
0 ignored issues
show
Deprecated Code introduced by
The function LeKoala\Tabulator\TabulatorGrid::getInitScript() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

666
            Requirements::customScript(/** @scrutinizer ignore-deprecated */ $this->getInitScript());
Loading history...
667
        }
668
669
        // Skip modular behaviour
670
        if ($this->useConfigProvider || $this->useInitScript) {
671
            return FormField::Field($properties);
672
        }
673
        return parent::Field($properties);
674
    }
675
676
    public function ShowTools(): string
677
    {
678
        if (empty($this->tools)) {
679
            return '';
680
        }
681
        $html = '';
682
        $html .= '<div class="tabulator-tools">';
683
        $html .= '<div class="tabulator-tools-start">';
684
        foreach ($this->tools as $tool) {
685
            if ($tool['position'] != self::POS_START) {
686
                continue;
687
            }
688
            $html .= ($tool['tool'])->forTemplate();
689
        }
690
        $html .= '</div>';
691
        $html .= '<div class="tabulator-tools-end">';
692
        foreach ($this->tools as $tool) {
693
            if ($tool['position'] != self::POS_END) {
694
                continue;
695
            }
696
            $html .= ($tool['tool'])->forTemplate();
697
        }
698
        // Show bulk actions at the end
699
        if (!empty($this->bulkActions)) {
700
            $selectLabel = _t(__CLASS__ . ".BULKSELECT", "Select a bulk action");
701
            $confirmLabel = _t(__CLASS__ . ".BULKCONFIRM", "Go");
702
            $html .= "<select class=\"tabulator-bulk-select\">";
703
            $html .= "<option>" . $selectLabel . "</option>";
704
            foreach ($this->bulkActions as $bulkAction) {
705
                $v = $bulkAction->getName();
706
                $xhr = $bulkAction->getXhr();
707
                $destructive = $bulkAction->getDestructive();
708
                $html .= "<option value=\"$v\" data-xhr=\"$xhr\" data-destructive=\"$destructive\">" . $bulkAction->getLabel() . "</option>";
709
            }
710
            $html .= "</select>";
711
            $html .= "<button class=\"tabulator-bulk-confirm btn\">" . $confirmLabel . "</button>";
712
        }
713
        $html .= '</div>';
714
        $html .= '</div>';
715
        return $html;
716
    }
717
718
    public function JsonOptions(): string
719
    {
720
        $this->processLinks();
721
722
        $data = $this->list ?? [];
723
        if ($this->autoloadDataList && $data instanceof DataList) {
724
            $data = null;
725
        }
726
        $opts = $this->options;
727
        $opts['columnDefaults'] = $this->columnDefaults;
728
729
        if (empty($this->columns)) {
730
            $opts['autoColumns'] = true;
731
        } else {
732
            $opts['columns'] = array_values($this->columns);
733
        }
734
735
        if ($data && is_iterable($data)) {
736
            if ($data instanceof ArrayList) {
737
                $data = $data->toArray();
738
            } else {
739
                if (is_iterable($data) && !is_array($data)) {
740
                    $data = iterator_to_array($data);
741
                }
742
            }
743
            $opts['data'] = $data;
744
        }
745
746
        // i18n
747
        $locale = strtolower(str_replace('_', '-', i18n::get_locale()));
748
        $paginationTranslations = [
749
            "first" => _t("TabulatorPagination.first", "First"),
750
            "first_title" =>  _t("TabulatorPagination.first_title", "First Page"),
751
            "last" =>  _t("TabulatorPagination.last", "Last"),
752
            "last_title" => _t("TabulatorPagination.last_title", "Last Page"),
753
            "prev" => _t("TabulatorPagination.prev", "Previous"),
754
            "prev_title" =>  _t("TabulatorPagination.prev_title", "Previous Page"),
755
            "next" => _t("TabulatorPagination.next", "Next"),
756
            "next_title" =>  _t("TabulatorPagination.next_title", "Next Page"),
757
            "all" =>  _t("TabulatorPagination.all", "All"),
758
        ];
759
        // This will always default to last icon if present
760
        $customIcons = self::config()->custom_pagination_icons;
761
        if (!empty($customIcons)) {
762
            $paginationTranslations['first'] = $customIcons['first'] ?? "<<";
763
            $paginationTranslations['last'] = $customIcons['last'] ?? ">>";
764
            $paginationTranslations['prev'] = $customIcons['prev'] ?? "<";
765
            $paginationTranslations['next'] = $customIcons['next'] ?? ">";
766
        }
767
        $dataTranslations = [
768
            "loading" => _t("TabulatorData.loading", "Loading"),
769
            "error" => _t("TabulatorData.error", "Error"),
770
        ];
771
        $groupsTranslations = [
772
            "item" => _t("TabulatorGroups.item", "Item"),
773
            "items" => _t("TabulatorGroups.items", "Items"),
774
        ];
775
        $headerFiltersTranslations = [
776
            "default" => _t("TabulatorHeaderFilters.default", "filter column..."),
777
        ];
778
        $bulkActionsTranslations = [
779
            "no_action" => _t("TabulatorBulkActions.no_action", "Please select an action"),
780
            "no_records" => _t("TabulatorBulkActions.no_records", "Please select a record"),
781
            "destructive" => _t("TabulatorBulkActions.destructive", "Confirm destructive action ?"),
782
        ];
783
        $translations = [
784
            'data' => $dataTranslations,
785
            'groups' => $groupsTranslations,
786
            'pagination' => $paginationTranslations,
787
            'headerFilters' => $headerFiltersTranslations,
788
            'bulkActions' => $bulkActionsTranslations,
789
        ];
790
        $opts['locale'] = $locale;
791
        $opts['langs'] = [
792
            $locale => $translations
793
        ];
794
795
        // Apply state
796
        $state = $this->getState();
797
        if (!empty($state['filter'])) {
798
            // @link https://tabulator.info/docs/5.4/filter#initial
799
            // We need to split between global filters and header filters
800
            $allFilters = $state['filter'] ?? [];
801
            $globalFilters = [];
802
            $headerFilters = [];
803
            foreach ($allFilters as $allFilter) {
804
                if (strpos($allFilter['field'], '__') === 0) {
805
                    $globalFilters[] = $allFilter;
806
                } else {
807
                    $headerFilters[] = $allFilter;
808
                }
809
            }
810
            $opts['initialFilter'] = $globalFilters;
811
            $opts['initialHeaderFilter'] = $headerFilters;
812
        }
813
        if (!empty($state['sort'])) {
814
            // @link https://tabulator.info/docs/5.4/sort#initial
815
            $opts['initialSort'] = $state['sort'];
816
        }
817
        $opts['_state'] = $state;
818
819
        $format = Director::isDev() ? JSON_PRETTY_PRINT : 0;
820
        $json = json_encode($opts, $format);
821
822
        // Escape functions by namespace (see TabulatorField.js)
823
        foreach ($this->jsNamespaces as $ns) {
824
            $json = preg_replace('/"(' . $ns . '\.[a-zA-Z]*)"/', "$1", $json);
825
            // Keep static namespaces
826
            $json = str_replace("*" . $ns, $ns, $json);
827
        }
828
829
        return $json;
830
    }
831
832
    /**
833
     * @param Controller $controller
834
     * @return CompatLayerInterface
835
     */
836
    public function getCompatLayer(Controller $controller)
837
    {
838
        if (is_subclass_of($controller, \SilverStripe\Admin\LeftAndMain::class)) {
0 ignored issues
show
Bug introduced by
The type SilverStripe\Admin\LeftAndMain was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
839
            return new SilverstripeAdminCompat();
840
        }
841
        if (is_subclass_of($controller, \LeKoala\Admini\LeftAndMain::class)) {
0 ignored issues
show
Bug introduced by
The type LeKoala\Admini\LeftAndMain was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
842
            return new AdminiCompat();
843
        }
844
    }
845
846
    public function getAttributes()
847
    {
848
        $attrs = parent::getAttributes();
849
        unset($attrs['type']);
850
        unset($attrs['name']);
851
        unset($attrs['value']);
852
        return $attrs;
853
    }
854
855
    public function getOption(string $k)
856
    {
857
        return $this->options[$k] ?? null;
858
    }
859
860
    public function setOption(string $k, $v): self
861
    {
862
        $this->options[$k] = $v;
863
        return $this;
864
    }
865
866
    public function makeHeadersSticky(): self
867
    {
868
        $this->addExtraClass("tabulator-sticky");
869
        return $this;
870
    }
871
872
    public function setRemoteSource(string $url, array $extraParams = [], bool $dataResponse = false): self
873
    {
874
        $this->setOption("ajaxURL", $url); //set url for ajax request
875
        $params = array_merge([
876
            'SecurityID' => SecurityToken::getSecurityID()
877
        ], $extraParams);
878
        $this->setOption("ajaxParams", $params);
879
        // Accept response where data is nested under the data key
880
        if ($dataResponse) {
881
            $this->setOption("ajaxResponse", self::JS_DATA_AJAX_RESPONSE);
882
        }
883
        return $this;
884
    }
885
886
    /**
887
     * @link http://www.tabulator.info/docs/5.4/page#remote
888
     * @param string $url
889
     * @param array $params
890
     * @param integer $pageSize
891
     * @param integer $initialPage
892
     */
893
    public function setRemotePagination(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1): self
894
    {
895
        $this->setOption("pagination", true); //enable pagination
896
        $this->setOption("paginationMode", 'remote'); //enable remote pagination
897
        $this->setRemoteSource($url, $params);
898
        if (!$pageSize) {
899
            $pageSize = $this->pageSize;
900
        }
901
        $this->setOption("paginationSize", $pageSize);
902
        $this->setOption("paginationInitialPage", $initialPage);
903
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/5.4/page#counter
904
        return $this;
905
    }
906
907
    public function wizardRemotePagination(int $pageSize = 0, int $initialPage = 1, array $params = []): self
908
    {
909
        $this->setRemotePagination($this->TempLink('load', false), $params, $pageSize, $initialPage);
910
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/5.4/sort#ajax-sort
911
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/5.4/filter#ajax-filter
912
        return $this;
913
    }
914
915
    public function setProgressiveLoad(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0): self
916
    {
917
        $this->setOption("ajaxURL", $url);
918
        if (!empty($params)) {
919
            $this->setOption("ajaxParams", $params);
920
        }
921
        $this->setOption("progressiveLoad", $mode);
922
        if ($scrollMargin > 0) {
923
            $this->setOption("progressiveLoadScrollMargin", $scrollMargin);
924
        }
925
        if (!$pageSize) {
926
            $pageSize = $this->pageSize;
927
        }
928
        $this->setOption("paginationSize", $pageSize);
929
        $this->setOption("paginationInitialPage", $initialPage);
930
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/5.4/page#counter
931
        return $this;
932
    }
933
934
    public function wizardProgressiveLoad(int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0, array $extraParams = []): self
935
    {
936
        $params = array_merge([
937
            'SecurityID' => SecurityToken::getSecurityID()
938
        ], $extraParams);
939
        $this->setProgressiveLoad($this->TempLink('load', false), $params, $pageSize, $initialPage, $mode, $scrollMargin);
940
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/5.4/sort#ajax-sort
941
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/5.4/filter#ajax-filter
942
        return $this;
943
    }
944
945
    /**
946
     * @link https://tabulator.info/docs/5.4/layout#responsive
947
     * @param boolean $startOpen
948
     * @param string $mode collapse|hide|flexCollapse
949
     * @return self
950
     */
951
    public function wizardResponsiveCollapse(bool $startOpen = false, string $mode = "collapse"): self
952
    {
953
        $this->setOption("responsiveLayout", $mode);
954
        $this->setOption("responsiveLayoutCollapseStartOpen", $startOpen);
955
        if ($mode != "hide") {
956
            $this->columns = array_merge([
957
                'ui_responsive_collapse' => [
958
                    "cssClass" => 'tabulator-cell-btn',
959
                    'formatter' => 'responsiveCollapse',
960
                    'headerSort' => false,
961
                    'width' => 40,
962
                ]
963
            ], $this->columns);
964
        }
965
        return $this;
966
    }
967
968
    public function wizardDataTree(bool $startExpanded = false, bool $filter = false, bool $sort = false, string $el = null): self
969
    {
970
        $this->setOption("dataTree", true);
971
        $this->setOption("dataTreeStartExpanded", $startExpanded);
972
        $this->setOption("dataTreeFilter", $filter);
973
        $this->setOption("dataTreeSort", $sort);
974
        if ($el) {
975
            $this->setOption("dataTreeElementColumn", $el);
976
        }
977
        return $this;
978
    }
979
980
    public function wizardSelectable(array $actions = []): self
981
    {
982
        $this->columns = array_merge([
983
            'ui_selectable' => [
984
                "hozAlign" => 'center',
985
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector',
986
                'formatter' => 'rowSelection',
987
                'titleFormatter' => 'rowSelection',
988
                'headerSort' => false,
989
                'width' => 40,
990
                'cellClick' => 'SSTabulator.forwardClick',
991
            ]
992
        ], $this->columns);
993
        $this->setBulkActions($actions);
994
        return $this;
995
    }
996
997
    public function wizardMoveable(string $callback = "SSTabulator.rowMoved"): self
998
    {
999
        $this->moveUrl = $this->TempLink("item/{ID}/ajaxMove", false);
1000
        $this->setOption("movableRows", true);
1001
        $this->addListener("rowMoved", $callback);
1002
        $this->columns = array_merge([
1003
            'ui_move' => [
1004
                "hozAlign" => 'center',
1005
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector',
1006
                'rowHandle' => true,
1007
                'formatter' => 'handle',
1008
                'headerSort' => false,
1009
                'frozen' => true,
1010
                'width' => 40,
1011
            ]
1012
        ], $this->columns);
1013
        return $this;
1014
    }
1015
1016
    /**
1017
     * @param string $field
1018
     * @param string $toggleElement arrow|header|false (header by default)
1019
     * @param boolean $isBool
1020
     * @return void
1021
     */
1022
    public function wizardGroupBy(string $field, string $toggleElement = 'header', bool $isBool = false)
1023
    {
1024
        $this->setOption("groupBy", $field);
1025
        $this->setOption("groupToggleElement", $toggleElement);
1026
        if ($isBool) {
1027
            $this->setOption("groupHeader", self::JS_BOOL_GROUP_HEADER);
1028
        }
1029
    }
1030
1031
    /**
1032
     * @param HTTPRequest $request
1033
     * @return HTTPResponse
1034
     */
1035
    public function handleItem($request)
1036
    {
1037
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1038
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1039
        $requestHandler = $this->getForm()->getController();
1040
        $record = $this->getRecordFromRequest($request);
1041
        if (!$record) {
1042
            return $requestHandler->httpError(404, 'That record was not found');
1043
        }
1044
        $handler = $this->getItemRequestHandler($record, $requestHandler);
1045
        return $handler->handleRequest($request);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $handler->handleRequest($request) also could return the type SilverStripe\Control\RequestHandler|array|string which is incompatible with the documented return type SilverStripe\Control\HTTPResponse.
Loading history...
1046
    }
1047
1048
    /**
1049
     * @param HTTPRequest $request
1050
     * @return HTTPResponse
1051
     */
1052
    public function handleTool($request)
1053
    {
1054
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1055
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1056
        $requestHandler = $this->getForm()->getController();
1057
        $tool = $this->getToolFromRequest($request);
1058
        if (!$tool) {
1059
            return $requestHandler->httpError(404, 'That tool was not found');
1060
        }
1061
        return $tool->handleRequest($request);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $tool->handleRequest($request) also could return the type SilverStripe\Control\RequestHandler|array|string which is incompatible with the documented return type SilverStripe\Control\HTTPResponse.
Loading history...
1062
    }
1063
1064
    /**
1065
     * @param HTTPRequest $request
1066
     * @return HTTPResponse
1067
     */
1068
    public function handleBulkAction($request)
1069
    {
1070
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1071
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1072
        $requestHandler = $this->getForm()->getController();
1073
        $bulkAction = $this->getBulkActionFromRequest($request);
1074
        if (!$bulkAction) {
1075
            return $requestHandler->httpError(404, 'That bulk action was not found');
1076
        }
1077
        return $bulkAction->handleRequest($request);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $bulkAction->handleRequest($request) also could return the type SilverStripe\Control\RequestHandler|array|string which is incompatible with the documented return type SilverStripe\Control\HTTPResponse.
Loading history...
1078
    }
1079
1080
    /**
1081
     * @return string name of {@see TabulatorGrid_ItemRequest} subclass
1082
     */
1083
    public function getItemRequestClass(): string
1084
    {
1085
        if ($this->itemRequestClass) {
1086
            return $this->itemRequestClass;
1087
        } elseif (ClassInfo::exists(static::class . '_ItemRequest')) {
1088
            return static::class . '_ItemRequest';
1089
        }
1090
        return TabulatorGrid_ItemRequest::class;
1091
    }
1092
1093
    /**
1094
     * Build a request handler for the given record
1095
     *
1096
     * @param DataObject $record
1097
     * @param RequestHandler $requestHandler
1098
     * @return TabulatorGrid_ItemRequest
1099
     */
1100
    protected function getItemRequestHandler($record, $requestHandler)
1101
    {
1102
        $class = $this->getItemRequestClass();
1103
        $assignedClass = $this->itemRequestClass;
1104
        $this->extend('updateItemRequestClass', $class, $record, $requestHandler, $assignedClass);
1105
        /** @var TabulatorGrid_ItemRequest $handler */
1106
        $handler = Injector::inst()->createWithArgs(
1107
            $class,
1108
            [$this, $record, $requestHandler]
1109
        );
1110
        if ($template = $this->getTemplate()) {
1111
            $handler->setTemplate($template);
1112
        }
1113
        $this->extend('updateItemRequestHandler', $handler);
1114
        return $handler;
1115
    }
1116
1117
    public function getStateKey()
1118
    {
1119
        $nested = [];
1120
        $form = $this->getForm();
1121
        $scope = $this->modelClass ? str_replace('_', '\\', $this->modelClass) :  "default";
1122
        if ($form) {
0 ignored issues
show
introduced by
$form is of type SilverStripe\Forms\Form, thus it always evaluated to true.
Loading history...
1123
            $controller = $form->getController();
1124
1125
            // We are in a nested form, track by id since each records needs it own state
1126
            while ($controller instanceof TabulatorGrid_ItemRequest) {
1127
                $record = $controller->getRecord();
1128
                $nested[str_replace('_', '\\', get_class($record))] = $record->ID;
1129
1130
                // Move to parent controller
1131
                $controller = $controller->getController();
1132
            }
1133
1134
            // Scope by top controller class
1135
            $scope = str_replace('_', '\\', get_class($controller));
1136
        }
1137
1138
        $prefix = "TabulatorState";
1139
        $name = $this->getName();
1140
        $key = "$prefix.$scope.$name";
1141
        foreach ($nested as $k => $v) {
1142
            $key .= "$k.$v";
1143
        }
1144
        return $key;
1145
    }
1146
1147
    /**
1148
     * @param HTTPRequest|null $request
1149
     * @return array{'page': int, 'limit': int, 'sort': array, 'filter': array}
1150
     */
1151
    public function getState(HTTPRequest $request = null)
1152
    {
1153
        if ($request === null) {
1154
            $request = Controller::curr()->getRequest();
1155
        }
1156
        $stateKey = $this->getStateKey();
1157
        $state = $request->getSession()->get($stateKey);
1158
        return $state ?? [
1159
            'page' => 1,
1160
            'limit' => $this->pageSize,
1161
            'sort' => [],
1162
            'filter' => [],
1163
        ];
1164
    }
1165
1166
    public function setState(HTTPRequest $request, $state)
1167
    {
1168
        $stateKey = $this->getStateKey();
1169
        $request->getSession()->set($stateKey, $state);
1170
        // If we are in a new controller, we can clear other states
1171
        $matches = [];
1172
        preg_match_all('/\.(.*?)\./', $stateKey, $matches);
1173
        $scope = $matches[1][0] ?? null;
1174
        if ($scope) {
1175
            self::clearAllStates($scope);
1176
        }
1177
    }
1178
1179
    public function clearState(HTTPRequest $request)
1180
    {
1181
        $stateKey = $this->getStateKey();
1182
        $request->getSession()->clear($stateKey);
1183
    }
1184
1185
    public static function clearAllStates(string $exceptScope = null)
1186
    {
1187
        $request = Controller::curr()->getRequest();
1188
        $allStates = $request->getSession()->get('TabulatorState');
1189
        if (!$allStates) {
1190
            return;
1191
        }
1192
        foreach ($allStates as $scope => $data) {
1193
            if ($exceptScope && $scope == $exceptScope) {
1194
                continue;
1195
            }
1196
            $request->getSession()->clear("TabulatorState.$scope");
1197
        }
1198
    }
1199
1200
    public function StateValue($key, $field): ?string
1201
    {
1202
        $state = $this->getState();
1203
        $arr = $state[$key] ?? [];
1204
        foreach ($arr as $s) {
1205
            if ($s['field'] === $field) {
1206
                return $s['value'];
1207
            }
1208
        }
1209
        return null;
1210
    }
1211
1212
    /**
1213
     * Deprecated in favor of modular behaviour
1214
     * @deprecated
1215
     * @return string
1216
     */
1217
    public function getInitScript(): string
1218
    {
1219
        $JsonOptions = $this->JsonOptions();
1220
        $ID = $this->ID();
1221
        $script = "SSTabulator.init(\"#$ID\", $JsonOptions);";
1222
        return $script;
1223
    }
1224
1225
    /**
1226
     * Provides the configuration for this instance
1227
     *
1228
     * This is really useful in the context of the admin as it will be served over
1229
     * ajax
1230
     *
1231
     * Deprecated in favor of modular behaviour
1232
     * @deprecated
1233
     * @param HTTPRequest $request
1234
     * @return HTTPResponse
1235
     */
1236
    public function configProvider(HTTPRequest $request)
1237
    {
1238
        if (!$this->useConfigProvider) {
1239
            return $this->httpError(404);
1240
        }
1241
        $response = new HTTPResponse($this->getInitScript());
0 ignored issues
show
Deprecated Code introduced by
The function LeKoala\Tabulator\TabulatorGrid::getInitScript() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1241
        $response = new HTTPResponse(/** @scrutinizer ignore-deprecated */ $this->getInitScript());
Loading history...
1242
        $response->addHeader('Content-Type', 'application/script');
1243
        return $response;
1244
    }
1245
1246
    /**
1247
     * Provides autocomplete lists
1248
     *
1249
     * @param HTTPRequest $request
1250
     * @return HTTPResponse
1251
     */
1252
    public function autocomplete(HTTPRequest $request)
1253
    {
1254
        if ($this->isDisabled() || $this->isReadonly()) {
1255
            return $this->httpError(403);
1256
        }
1257
        $SecurityID = $request->getVar('SecurityID');
1258
        if (!SecurityToken::inst()->check($SecurityID)) {
1259
            return $this->httpError(404, "Invalid SecurityID");
1260
        }
1261
1262
        $name = $request->getVar("Column");
1263
        $col = $this->getColumn($name);
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type null; however, parameter $key of LeKoala\Tabulator\TabulatorGrid::getColumn() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1263
        $col = $this->getColumn(/** @scrutinizer ignore-type */ $name);
Loading history...
1264
        if (!$col) {
1265
            return $this->httpError(403, "Invalid column");
1266
        }
1267
1268
        // Don't use % term as it prevents use of indexes
1269
        $term = $request->getVar('term') . '%';
1270
        $term = str_replace(' ', '%', $term);
1271
1272
        $parts = explode(".", $name);
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1272
        $parts = explode(".", /** @scrutinizer ignore-type */ $name);
Loading history...
1273
        if (count($parts) > 2) {
1274
            array_pop($parts);
1275
        }
1276
        if (count($parts) == 2) {
1277
            $class = $parts[0];
1278
            $field = $parts[1];
1279
        } elseif (count($parts) == 1) {
1280
            $class = preg_replace("/ID$/", "", $parts[0]);
1281
            $field = 'Title';
1282
        } else {
1283
            return $this->httpError(403, "Invalid field");
1284
        }
1285
1286
        /** @var DataObject $sng */
1287
        $sng = $class::singleton();
1288
        $baseTable = $sng->baseTable();
1289
1290
        $searchField = null;
1291
        $searchCandidates = [
1292
            $field, 'Name', 'Surname', 'Email', 'ID'
1293
        ];
1294
1295
        // Ensure field exists, this is really rudimentary
1296
        $db = $class::config()->db;
1297
        foreach ($searchCandidates as $searchCandidate) {
1298
            if ($searchField) {
1299
                continue;
1300
            }
1301
            if (isset($db[$searchCandidate])) {
1302
                $searchField = $searchCandidate;
1303
            }
1304
        }
1305
        $searchCols = [$searchField];
1306
1307
        // For members, do something better
1308
        if ($baseTable == 'Member') {
1309
            $searchField = ['FirstName', 'Surname'];
1310
            $searchCols = ['FirstName', 'Surname', 'Email'];
1311
        }
1312
1313
        if (!empty($col['editorParams']['customSearchField'])) {
1314
            $searchField = $col['editorParams']['customSearchField'];
1315
        }
1316
        if (!empty($col['editorParams']['customSearchCols'])) {
1317
            $searchCols = $col['editorParams']['customSearchCols'];
1318
        }
1319
1320
1321
        /** @var DataList $list */
1322
        $list = $sng::get();
1323
1324
        // Make sure at least one field is not null...
1325
        $where = [];
1326
        foreach ($searchCols as $searchCol) {
1327
            $where[] = $searchCol . ' IS NOT NULL';
1328
        }
1329
        $list = $list->where($where);
1330
        // ... and matches search term ...
1331
        $where = [];
1332
        foreach ($searchCols as $searchCol) {
1333
            $where[$searchCol . ' LIKE ?'] = $term;
1334
        }
1335
        $list = $list->whereAny($where);
1336
1337
        // ... and any user set requirements
1338
        if (!empty($col['editorParams']['where'])) {
1339
            // Deal with in clause
1340
            $customWhere = [];
1341
            foreach ($col['editorParams']['where'] as $col => $param) {
1342
                // For array, we need a IN statement with a ? for each value
1343
                if (is_array($param)) {
1344
                    $prepValue = [];
1345
                    $params = [];
1346
                    foreach ($param as $paramValue) {
1347
                        $params[] = $paramValue;
1348
                        $prepValue[] = "?";
1349
                    }
1350
                    $customWhere["$col IN (" . implode(',', $prepValue) . ")"] = $params;
1351
                } else {
1352
                    $customWhere["$col = ?"] = $param;
1353
                }
1354
            }
1355
            $list = $list->where($customWhere);
1356
        }
1357
1358
        $results = iterator_to_array($list);
1359
        $data = [];
1360
        foreach ($results as $record) {
1361
            if (is_array($searchField)) {
1362
                $labelParts = [];
1363
                foreach ($searchField as $sf) {
1364
                    $labelParts[] = $record->$sf;
1365
                }
1366
                $label = implode(" ", $labelParts);
1367
            } else {
1368
                $label = $record->$searchField;
1369
            }
1370
            $data[] = [
1371
                'value' => $record->ID,
1372
                'label' => $label,
1373
            ];
1374
        }
1375
1376
        $json = json_encode($data);
1377
        $response = new HTTPResponse($json);
1378
        $response->addHeader('Content-Type', 'application/script');
1379
        return $response;
1380
    }
1381
1382
    /**
1383
     * @link http://www.tabulator.info/docs/5.4/page#remote-response
1384
     * @param HTTPRequest $request
1385
     * @return HTTPResponse
1386
     */
1387
    public function load(HTTPRequest $request)
1388
    {
1389
        if ($this->isDisabled() || $this->isReadonly()) {
1390
            return $this->httpError(403);
1391
        }
1392
        $SecurityID = $request->getVar('SecurityID');
1393
        if (!SecurityToken::inst()->check($SecurityID)) {
1394
            return $this->httpError(404, "Invalid SecurityID");
1395
        }
1396
1397
        $page = (int) $request->getVar('page');
1398
        $limit = (int) $request->getVar('size');
1399
1400
        $sort = $request->getVar('sort');
1401
        $filter = $request->getVar('filter');
1402
1403
        // Persist state to allow the ItemEditForm to display navigation
1404
        $state = [
1405
            'page' => $page,
1406
            'limit' => $limit,
1407
            'sort' => $sort,
1408
            'filter' => $filter,
1409
        ];
1410
        $this->setState($request, $state);
1411
1412
        $offset = ($page - 1) * $limit;
1413
        $data = $this->getManipulatedData($limit, $offset, $sort, $filter);
1414
1415
        $encodedData = json_encode($data);
1416
        if (!$encodedData) {
1417
            throw new Exception(json_last_error_msg());
1418
        }
1419
1420
        $response = new HTTPResponse($encodedData);
1421
        $response->addHeader('Content-Type', 'application/json');
1422
        return $response;
1423
    }
1424
1425
    /**
1426
     * @param HTTPRequest $request
1427
     * @return DataObject|null
1428
     */
1429
    protected function getRecordFromRequest(HTTPRequest $request): ?DataObject
1430
    {
1431
        /** @var DataObject $record */
1432
        if (is_numeric($request->param('ID'))) {
1433
            /** @var Filterable $dataList */
1434
            $dataList = $this->getList();
1435
            $record = $dataList->byID($request->param('ID'));
1436
        } else {
1437
            $record = Injector::inst()->create($this->getModelClass());
1438
        }
1439
        return $record;
1440
    }
1441
1442
    /**
1443
     * @param HTTPRequest $request
1444
     * @return AbstractTabulatorTool|null
1445
     */
1446
    protected function getToolFromRequest(HTTPRequest $request): ?AbstractTabulatorTool
1447
    {
1448
        $toolID = $request->param('ID');
1449
        $tool = $this->getTool($toolID);
1450
        return $tool;
1451
    }
1452
1453
    /**
1454
     * @param HTTPRequest $request
1455
     * @return AbstractBulkAction|null
1456
     */
1457
    protected function getBulkActionFromRequest(HTTPRequest $request): ?AbstractBulkAction
1458
    {
1459
        $toolID = $request->param('ID');
1460
        $tool = $this->getBulkAction($toolID);
1461
        return $tool;
1462
    }
1463
1464
    /**
1465
     * Get the value of a named field  on the given record.
1466
     *
1467
     * Use of this method ensures that any special rules around the data for this gridfield are
1468
     * followed.
1469
     *
1470
     * @param DataObject $record
1471
     * @param string $fieldName
1472
     *
1473
     * @return mixed
1474
     */
1475
    public function getDataFieldValue($record, $fieldName)
1476
    {
1477
        if ($record->hasMethod('relField')) {
1478
            return $record->relField($fieldName);
1479
        }
1480
1481
        if ($record->hasMethod($fieldName)) {
1482
            return $record->$fieldName();
1483
        }
1484
1485
        return $record->$fieldName;
1486
    }
1487
1488
    public function getManipulatedList(): SS_List
1489
    {
1490
        return $this->list;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->list could return the type null which is incompatible with the type-hinted return SilverStripe\ORM\SS_List. Consider adding an additional type-check to rule them out.
Loading history...
1491
    }
1492
1493
    public function getList(): SS_List
1494
    {
1495
        return $this->list;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->list could return the type null which is incompatible with the type-hinted return SilverStripe\ORM\SS_List. Consider adding an additional type-check to rule them out.
Loading history...
1496
    }
1497
1498
    public function setList(SS_List $list): self
1499
    {
1500
        if ($this->autoloadDataList && $list instanceof DataList) {
1501
            $this->wizardRemotePagination();
1502
        }
1503
        $this->list = $list;
1504
        return $this;
1505
    }
1506
1507
    public function hasArrayList(): bool
1508
    {
1509
        return $this->list instanceof ArrayList;
1510
    }
1511
1512
    public function getArrayList(): ArrayList
1513
    {
1514
        if (!$this->list instanceof ArrayList) {
1515
            throw new RuntimeException("Value is not a ArrayList, it is a: " . get_class($this->list));
0 ignored issues
show
Bug introduced by
It seems like $this->list can also be of type null; however, parameter $object of get_class() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1515
            throw new RuntimeException("Value is not a ArrayList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1516
        }
1517
        return $this->list;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->list returns the type null which is incompatible with the type-hinted return SilverStripe\ORM\ArrayList.
Loading history...
1518
    }
1519
1520
    public function hasDataList(): bool
1521
    {
1522
        return $this->list instanceof DataList;
1523
    }
1524
1525
    /**
1526
     * A properly typed on which you can call byID
1527
     * @return ArrayList|DataList
1528
     */
1529
    public function getByIDList()
1530
    {
1531
        return $this->list;
1532
    }
1533
1534
    public function hasByIDList(): bool
1535
    {
1536
        return $this->hasDataList() || $this->hasArrayList();
1537
    }
1538
1539
    public function getDataList(): DataList
1540
    {
1541
        if (!$this->list instanceof DataList) {
1542
            throw new RuntimeException("Value is not a DataList, it is a: " . get_class($this->list));
0 ignored issues
show
Bug introduced by
It seems like $this->list can also be of type null; however, parameter $object of get_class() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1542
            throw new RuntimeException("Value is not a DataList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1543
        }
1544
        return $this->list;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->list returns the type null which is incompatible with the type-hinted return SilverStripe\ORM\DataList.
Loading history...
1545
    }
1546
1547
    public function getManipulatedData(int $limit, int $offset, array $sort = null, array $filter = null): array
1548
    {
1549
        if (!$this->hasDataList()) {
1550
            $data = $this->list->toNestedArray();
0 ignored issues
show
Bug introduced by
The method toNestedArray() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1550
            /** @scrutinizer ignore-call */ 
1551
            $data = $this->list->toNestedArray();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1551
1552
            $lastRow = $this->list->count();
1553
            $lastPage = ceil($lastRow / $limit);
1554
1555
            $result = [
1556
                'last_row' => $lastRow,
1557
                'last_page' => $lastPage,
1558
                'data' => $data,
1559
            ];
1560
1561
            return $result;
1562
        }
1563
1564
        $dataList = $this->getDataList();
1565
1566
        $schema = DataObject::getSchema();
1567
        $dataClass = $dataList->dataClass();
1568
        /** @var DataObject $singleton */
1569
        $singleton = singleton($dataClass);
1570
        $resolutionMap = [];
1571
1572
        $sortSql = [];
1573
        if ($sort) {
1574
            foreach ($sort as $sortValues) {
1575
                $cols = array_keys($this->columns);
1576
                $field = $sortValues['field'];
1577
                if (!in_array($field, $cols)) {
1578
                    throw new Exception("Invalid sort field: $field");
1579
                }
1580
                $dir = $sortValues['dir'];
1581
                if (!in_array($dir, ['asc', 'desc'])) {
1582
                    throw new Exception("Invalid sort dir: $dir");
1583
                }
1584
1585
                // Nested sort
1586
                if (strpos($field, '.') !== false) {
1587
                    $parts = explode(".", $field);
1588
1589
                    // Resolve relation only once in case of multiples similar keys
1590
                    if (!isset($resolutionMap[$parts[0]])) {
1591
                        $resolutionMap[$parts[0]] = $singleton->relObject($parts[0]);
1592
                    }
1593
                    // Not matching anything (maybe a formatting .Nice ?)
1594
                    if (!$resolutionMap[$parts[0]] || !($resolutionMap[$parts[0]] instanceof DataList)) {
1595
                        $field = $parts[0];
0 ignored issues
show
Unused Code introduced by
The assignment to $field is dead and can be removed.
Loading history...
1596
                        continue;
1597
                    }
1598
                    $relatedObject = get_class($resolutionMap[$parts[0]]);
1599
                    $tableName = $schema->tableForField($relatedObject, $parts[1]);
1600
                    $baseIDColumn = $schema->sqlColumnForField($dataClass, 'ID');
1601
                    $tableAlias = $parts[0];
1602
                    $dataList = $dataList->leftJoin($tableName, "\"{$tableAlias}\".\"ID\" = {$baseIDColumn}", $tableAlias);
1603
                }
1604
1605
                $sortSql[] = $field . ' ' . $dir;
1606
            }
1607
        }
1608
        if (!empty($sortSql)) {
1609
            $dataList = $dataList->sort(implode(", ", $sortSql));
1610
        }
1611
1612
        // Filtering is an array of field/type/value arrays
1613
        $where = [];
1614
        $anyWhere = [];
1615
        if ($filter) {
1616
            foreach ($filter as $filterValues) {
1617
                $cols = array_keys($this->columns);
1618
                $field = $filterValues['field'];
1619
                if (strpos($field, '__') !== 0 && !in_array($field, $cols)) {
1620
                    throw new Exception("Invalid filter field: $field");
1621
                }
1622
                $value = $filterValues['value'];
1623
                $type = $filterValues['type'];
1624
1625
                $rawValue = $value;
1626
1627
                // Strict value
1628
                if ($value === "true") {
1629
                    $value = true;
1630
                } elseif ($value === "false") {
1631
                    $value = false;
1632
                }
1633
1634
                switch ($type) {
1635
                    case "=":
1636
                        // It's a wildcard search
1637
                        if ($field === "__wildcard") {
1638
                            $anyWhere = $this->createWildcardFilters($rawValue);
1639
                        } elseif ($field === "__quickfilter") {
1640
                            $this->createQuickFilter($rawValue, $dataList);
1641
                        } else {
1642
                            $where["$field"] = $value;
1643
                        }
1644
                        break;
1645
                    case "!=":
1646
                        $where["$field:not"] = $value;
1647
                        break;
1648
                    case "like":
1649
                        $where["$field:PartialMatch:nocase"] = $value;
1650
                        break;
1651
                    case "keywords":
1652
                        $where["$field:PartialMatch:nocase"] = str_replace(" ", "%", $value);
1653
                        break;
1654
                    case "starts":
1655
                        $where["$field:StartsWith:nocase"] = $value;
1656
                        break;
1657
                    case "ends":
1658
                        $where["$field:EndsWith:nocase"] = $value;
1659
                        break;
1660
                    case "<":
1661
                        $where["$field:LessThan:nocase"] = $value;
1662
                        break;
1663
                    case "<=":
1664
                        $where["$field:LessThanOrEqual:nocase"] = $value;
1665
                        break;
1666
                    case ">":
1667
                        $where["$field:GreaterThan:nocase"] = $value;
1668
                        break;
1669
                    case ">=":
1670
                        $where["$field:GreaterThanOrEqual:nocase"] = $value;
1671
                        break;
1672
                    case "in":
1673
                        $where["$field"] = $value;
1674
                        break;
1675
                    case "regex":
1676
                        $dataList = $dataList->where('REGEXP ' . Convert::raw2sql($value));
1677
                        break;
1678
                    default:
1679
                        throw new Exception("Invalid sort dir: $dir");
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $dir does not seem to be defined for all execution paths leading up to this point.
Loading history...
1680
                }
1681
            }
1682
        }
1683
        if (!empty($where)) {
1684
            $dataList = $dataList->filter($where);
1685
        }
1686
        if (!empty($anyWhere)) {
1687
            $dataList = $dataList->filterAny($anyWhere);
1688
        }
1689
1690
        $lastRow = $dataList->count();
1691
        $lastPage = ceil($lastRow / $limit);
1692
1693
        $data = [];
1694
        /** @var DataObject $record */
1695
        foreach ($dataList->limit($limit, $offset) as $record) {
1696
            if ($record->hasMethod('canView') && !$record->canView()) {
1697
                continue;
1698
            }
1699
1700
            $item = [
1701
                'ID' => $record->ID,
1702
            ];
1703
1704
            // Add row class
1705
            if ($record->hasMethod('TabulatorRowClass')) {
1706
                $item['_class'] = $record->TabulatorRowClass();
1707
            } elseif ($record->hasMethod('getRowClass')) {
1708
                $item['_class'] = $record->getRowClass();
1709
            }
1710
            // Add row color
1711
            if ($record->hasMethod('TabulatorRowColor')) {
1712
                $item['_color'] = $record->TabulatorRowColor();
1713
            }
1714
1715
            $nested = [];
1716
            foreach ($this->columns as $col) {
1717
                if (empty($col['field'])) {
1718
                    continue;
1719
                }
1720
                $field = $col['field'];
1721
                if (strpos($field, '.') !== false) {
1722
                    $parts = explode('.', $field);
1723
                    $classOrField = $parts[0];
1724
                    $relationOrMethod = $parts[1];
1725
                    // For relations, like Users.count
1726
                    if ($singleton->getRelationClass($classOrField)) {
1727
                        $nested[$classOrField][] = $relationOrMethod;
1728
                        continue;
1729
                    } else {
1730
                        // For fields, like SomeValue.Nice
1731
                        $dbObject = $record->dbObject($classOrField);
1732
                        if ($dbObject) {
1733
                            $item[$classOrField] = [
1734
                                $relationOrMethod => $dbObject->$relationOrMethod()
1735
                            ];
1736
                            continue;
1737
                        }
1738
                    }
1739
                }
1740
                if (!isset($item[$field])) {
1741
                    if ($record->hasMethod($field)) {
1742
                        $item[$field] = $record->$field();
1743
                    } else {
1744
                        $item[$field] = $record->getField($field);
1745
                    }
1746
                }
1747
            }
1748
            // Fill in nested data, like Users.count
1749
            foreach ($nested as $nestedClass => $nestedColumns) {
1750
                /** @var DataObject $relObject */
1751
                $relObject = $record->relObject($nestedClass);
1752
                $nestedData = [];
1753
                foreach ($nestedColumns as $nestedColumn) {
1754
                    $nestedData[$nestedColumn] = $this->getDataFieldValue($relObject, $nestedColumn);
1755
                }
1756
                $item[$nestedClass] = $nestedData;
1757
            }
1758
            $data[] = $item;
1759
        }
1760
1761
        $result = [
1762
            'last_row' => $lastRow,
1763
            'last_page' => $lastPage,
1764
            'data' => $data,
1765
        ];
1766
1767
        if (Director::isDev()) {
1768
            $result['sql'] = $dataList->sql();
1769
        }
1770
1771
        return $result;
1772
    }
1773
1774
    public function QuickFiltersList()
1775
    {
1776
        $current = $this->StateValue('filter', '__quickfilter');
1777
        $list = new ArrayList();
1778
        foreach ($this->quickFilters as $k => $v) {
1779
            $list->push([
1780
                'Value' => $k,
1781
                'Label' => $v['label'],
1782
                'Selected' => $k == $current
1783
            ]);
1784
        }
1785
        return $list;
1786
    }
1787
1788
    protected function createQuickFilter($filter, &$list)
1789
    {
1790
        $qf = $this->quickFilters[$filter] ?? null;
1791
        if (!$qf) {
1792
            return;
1793
        }
1794
1795
        $callback = $qf['callback'] ?? null;
1796
        if (!$callback) {
1797
            return;
1798
        }
1799
1800
        $callback($list);
1801
    }
1802
1803
    protected function createWildcardFilters(string $value)
1804
    {
1805
        $wildcardFields = $this->wildcardFields;
1806
1807
        // Create from model
1808
        if (empty($wildcardFields)) {
1809
            /** @var DataObject $singl */
1810
            $singl = singleton($this->modelClass);
1811
            $searchableFields = $singl->searchableFields();
1812
1813
            foreach ($searchableFields as $k => $v) {
1814
                $general = $v['general'] ?? true;
1815
                if (!$general) {
1816
                    continue;
1817
                }
1818
                $wildcardFields[] = $k;
1819
            }
1820
        }
1821
1822
        // Queries can have the format s: ... or e:... or =:....
1823
        $filter = 'PartialMatch';
1824
        if (strpos($value, ':') === 1) {
1825
            $parts = explode(":", $value);
1826
            $shortcut = array_shift($parts);
1827
            $value = implode(":", $parts);
1828
            switch ($shortcut) {
1829
                case 's':
1830
                    $filter = 'StartsWith';
1831
                    break;
1832
                case 'e':
1833
                    $filter = 'EndsWith';
1834
                    break;
1835
                case '=':
1836
                    $filter = 'ExactMatch';
1837
                    break;
1838
            }
1839
        }
1840
1841
        // Process value
1842
        $baseValue = $value;
1843
        $value = str_replace(" ", "%", $value);
1844
        $value = str_replace(['.', '_', '-'], ' ', $value);
1845
1846
        // Create filters
1847
        $anyWhere = [];
1848
        foreach ($wildcardFields as $f) {
1849
            if (!$value) {
1850
                continue;
1851
            }
1852
            $key = $f . ":" . $filter;
1853
            $anyWhere[$key] = $value;
1854
1855
            // also look on unfiltered data
1856
            if ($value != $baseValue) {
1857
                $anyWhere[$key] = $baseValue;
1858
            }
1859
        }
1860
1861
        return $anyWhere;
1862
    }
1863
1864
    public function getModelClass(): ?string
1865
    {
1866
        if ($this->modelClass) {
1867
            return $this->modelClass;
1868
        }
1869
        if ($this->list && $this->list instanceof DataList) {
1870
            return $this->list->dataClass();
1871
        }
1872
        return null;
1873
    }
1874
1875
    public function setModelClass(string $modelClass): self
1876
    {
1877
        $this->modelClass = $modelClass;
1878
        return $this;
1879
    }
1880
1881
1882
    public function getDataAttribute(string $k)
1883
    {
1884
        if (isset($this->dataAttributes[$k])) {
1885
            return $this->dataAttributes[$k];
1886
        }
1887
        return $this->getAttribute("data-$k");
1888
    }
1889
1890
    public function setDataAttribute(string $k, $v): self
1891
    {
1892
        $this->dataAttributes[$k] = $v;
1893
        return $this;
1894
    }
1895
1896
    public function dataAttributesHTML(): string
1897
    {
1898
        $parts = [];
1899
        foreach ($this->dataAttributes as $k => $v) {
1900
            if (!$v) {
1901
                continue;
1902
            }
1903
            if (is_array($v)) {
1904
                $v = json_encode($v);
1905
            }
1906
            $parts[] = "data-$k='$v'";
1907
        }
1908
        return implode(" ", $parts);
1909
    }
1910
1911
    protected function processLink(string $url): string
1912
    {
1913
        // It's not necessary to process
1914
        if ($url == '#') {
1915
            return $url;
1916
        }
1917
        // It's a temporary link on the form
1918
        if (strpos($url, 'form:') === 0) {
1919
            return $this->Link(preg_replace('/^form:/', '', $url));
1920
        }
1921
        // It's a temporary link on the controller
1922
        if (strpos($url, 'controller:') === 0) {
1923
            return $this->ControllerLink(preg_replace('/^controller:/', '', $url));
1924
        }
1925
        // It's a custom protocol (mailto: etc)
1926
        if (strpos($url, ':') !== false) {
1927
            return $url;
1928
        }
1929
        return $url;
1930
    }
1931
1932
    protected function processLinks(): void
1933
    {
1934
        // Process editor and formatter links
1935
        foreach ($this->columns as $name => $params) {
1936
            if (!empty($params['formatterParams']['url'])) {
1937
                $url = $this->processLink($params['formatterParams']['url']);
1938
                $this->columns[$name]['formatterParams']['url'] = $url;
1939
            }
1940
            if (!empty($params['editorParams']['url'])) {
1941
                $url = $this->processLink($params['editorParams']['url']);
1942
                $this->columns[$name]['editorParams']['url'] = $url;
1943
            }
1944
            // Set valuesURL automatically if not already set
1945
            if (!empty($params['editorParams']['autocomplete'])) {
1946
                if (empty($params['editorParams']['valuesURL'])) {
1947
                    $params = [
1948
                        'Column' => $name,
1949
                        'SecurityID' => SecurityToken::getSecurityID(),
1950
                    ];
1951
                    $url = $this->Link('autocomplete') . '?' . http_build_query($params);
1952
                    $this->columns[$name]['editorParams']['valuesURL'] = $url;
1953
                    $this->columns[$name]['editorParams']['filterRemote'] = true;
1954
                }
1955
            }
1956
        }
1957
1958
        // Other links
1959
        $url = $this->getOption('ajaxURL');
1960
        if ($url) {
1961
            $this->setOption('ajaxURL', $this->processLink($url));
1962
        }
1963
    }
1964
1965
    public function makeButton(string $urlOrAction, string $icon, string $title): array
1966
    {
1967
        $opts = [
1968
            "responsive" => 0,
1969
            "cssClass" => 'tabulator-cell-btn',
1970
            "tooltip" => $title,
1971
            "formatter" => "SSTabulator.buttonFormatter",
1972
            "formatterParams" => [
1973
                "icon" => $icon,
1974
                "title" => $title,
1975
                "url" => $this->TempLink($urlOrAction), // On the controller by default
1976
            ],
1977
            "cellClick" => "SSTabulator.buttonHandler",
1978
            "width" => 70,
1979
            "hozAlign" => "center",
1980
            "headerSort" => false,
1981
1982
        ];
1983
        return $opts;
1984
    }
1985
1986
    public function addButtonFromArray(string $action, array $opts = [], string $before = null): self
1987
    {
1988
        // Insert before given column
1989
        if ($before) {
1990
            if (array_key_exists($before, $this->columns)) {
1991
                $new = [];
1992
                foreach ($this->columns as $k => $value) {
1993
                    if ($k === $before) {
1994
                        $new["action_$action"] = $opts;
1995
                    }
1996
                    $new[$k] = $value;
1997
                }
1998
                $this->columns = $new;
1999
            }
2000
        } else {
2001
            $this->columns["action_$action"] = $opts;
2002
        }
2003
        return $this;
2004
    }
2005
2006
    /**
2007
     * @param string $action Action name
2008
     * @param string $url Parameters between {} will be interpolated by row values.
2009
     * @param string $icon
2010
     * @param string $title
2011
     * @param string|null $before
2012
     * @return self
2013
     */
2014
    public function addButton(string $action, string $url, string $icon, string $title, string $before = null): self
2015
    {
2016
        $opts = $this->makeButton($url, $icon, $title);
2017
        $this->addButtonFromArray($action, $opts, $before);
2018
        return $this;
2019
    }
2020
2021
    public function shiftButton(string $action, string $url, string $icon, string $title): self
2022
    {
2023
        // Find first action
2024
        foreach ($this->columns as $name => $options) {
2025
            if (strpos($name, 'action_') === 0) {
2026
                return $this->addButton($action, $url, $icon, $title, $name);
2027
            }
2028
        }
2029
        return $this->addButton($action, $url, $icon, $title);
2030
    }
2031
2032
    public function removeButton(string $action): self
2033
    {
2034
        if (isset($this->columns["action_$action"])) {
2035
            unset($this->columns["action_$action"]);
2036
        }
2037
        return $this;
2038
    }
2039
2040
    /**
2041
     * @link http://www.tabulator.info/docs/5.4/columns#definition
2042
     * @param string $field (Required) this is the key for this column in the data array
2043
     * @param string $title (Required) This is the title that will be displayed in the header for this column
2044
     * @param array $opts Other options to merge in
2045
     * @return $this
2046
     */
2047
    public function addColumn(string $field, string $title = null, array $opts = []): self
2048
    {
2049
        if ($title === null) {
2050
            $title = $field;
2051
        }
2052
2053
        $baseOpts = [
2054
            "field" => $field,
2055
            "title" => $title,
2056
        ];
2057
2058
        if (!empty($opts)) {
2059
            $baseOpts = array_merge($baseOpts, $opts);
2060
        }
2061
2062
        $this->columns[$field] = $baseOpts;
2063
        return $this;
2064
    }
2065
2066
    /**
2067
     * @link http://www.tabulator.info/docs/5.4/columns#definition
2068
     * @param array $opts Other options to merge in
2069
     * @return $this
2070
     */
2071
    public function addColumnFromArray(array $opts = [])
2072
    {
2073
        if (empty($opts['field']) || !isset($opts['title'])) {
2074
            throw new Exception("Missing field or title key");
2075
        }
2076
        $field = $opts['field'];
2077
        $this->columns[$field] = $opts;
2078
        return $this;
2079
    }
2080
2081
    public function makeColumnEditable(string $field, string $editor = "input", array $params = [])
2082
    {
2083
        $col = $this->getColumn($field);
2084
        if (!$col) {
2085
            throw new InvalidArgumentException("$field is not a valid column");
2086
        }
2087
2088
        switch ($editor) {
2089
            case 'date':
2090
                $editor = "input";
2091
                $params = [
2092
                    'mask' => "9999-99-99",
2093
                    'maskAutoFill' => 'true',
2094
                ];
2095
                break;
2096
            case 'datetime':
2097
                $editor = "input";
2098
                $params = [
2099
                    'mask' => "9999-99-99 99:99:99",
2100
                    'maskAutoFill' => 'true',
2101
                ];
2102
                break;
2103
        }
2104
2105
        if (empty($col['cssClass'])) {
2106
            $col['cssClass'] = 'no-change-track';
2107
        } else {
2108
            $col['cssClass'] .= ' no-change-track';
2109
        }
2110
2111
        $col['editor'] = $editor;
2112
        $col['editorParams'] = $params;
2113
        if ($editor == "list") {
2114
            if (!empty($params['autocomplete'])) {
2115
                $col['headerFilter'] = "input"; // force input
2116
            } else {
2117
                $col['headerFilterParams'] = $params; // editor is used as base filter editor
2118
            }
2119
        }
2120
2121
2122
        $this->setColumn($field, $col);
2123
    }
2124
2125
    /**
2126
     * Get column details
2127
2128
     * @param string $key
2129
     */
2130
    public function getColumn(string $key): ?array
2131
    {
2132
        if (isset($this->columns[$key])) {
2133
            return $this->columns[$key];
2134
        }
2135
        return null;
2136
    }
2137
2138
    /**
2139
     * Set column details
2140
     *
2141
     * @param string $key
2142
     * @param array $col
2143
     */
2144
    public function setColumn(string $key, array $col): self
2145
    {
2146
        $this->columns[$key] = $col;
2147
        return $this;
2148
    }
2149
2150
    /**
2151
     * Update column details
2152
     *
2153
     * @param string $key
2154
     * @param array $col
2155
     */
2156
    public function updateColumn(string $key, array $col): self
2157
    {
2158
        $data = $this->getColumn($key);
2159
        if ($data) {
2160
            $this->setColumn($key, array_merge($data, $col));
2161
        }
2162
        return $this;
2163
    }
2164
2165
    /**
2166
     * Remove a column
2167
     *
2168
     * @param string $key
2169
     */
2170
    public function removeColumn(string $key): void
2171
    {
2172
        unset($this->columns[$key]);
2173
    }
2174
2175
2176
    /**
2177
     * Get the value of columns
2178
     */
2179
    public function getColumns(): array
2180
    {
2181
        return $this->columns;
2182
    }
2183
2184
    /**
2185
     * Set the value of columns
2186
     */
2187
    public function setColumns(array $columns): self
2188
    {
2189
        $this->columns = $columns;
2190
        return $this;
2191
    }
2192
2193
    /**
2194
     * @param string|AbstractTabulatorTool $tool Pass name or class
2195
     * @return AbstractTabulatorTool|null
2196
     */
2197
    public function getTool($tool): ?AbstractTabulatorTool
2198
    {
2199
        if (is_object($tool)) {
2200
            $tool = get_class($tool);
2201
        }
2202
        if (!is_string($tool)) {
0 ignored issues
show
introduced by
The condition is_string($tool) is always true.
Loading history...
2203
            throw new InvalidArgumentException('Tool must be an object or a class name');
2204
        }
2205
        foreach ($this->tools as $t) {
2206
            if ($t['name'] === $tool) {
2207
                return $t['tool'];
2208
            }
2209
            if ($t['tool'] instanceof $tool) {
2210
                return $t['tool'];
2211
            }
2212
        }
2213
        return null;
2214
    }
2215
2216
    public function addTool(string $pos, AbstractTabulatorTool $tool, string $name = ''): self
2217
    {
2218
        $tool->setTabulatorGrid($this);
2219
        $tool->setName($name);
2220
2221
        $this->tools[] = [
2222
            'position' => $pos,
2223
            'tool' => $tool,
2224
            'name' => $name,
2225
        ];
2226
        return $this;
2227
    }
2228
2229
    public function removeTool($tool): self
2230
    {
2231
        if (is_object($tool)) {
2232
            $tool = get_class($tool);
2233
        }
2234
        if (!is_string($tool)) {
2235
            throw new InvalidArgumentException('Tool must be an object or a class name');
2236
        }
2237
        foreach ($this->tools as $idx => $tool) {
0 ignored issues
show
introduced by
$tool is overwriting one of the parameters of this function.
Loading history...
2238
            if ($tool['name'] === $tool) {
2239
                unset($this->tools[$idx]);
2240
            }
2241
            if ($tool['tool'] instanceof $tool) {
2242
                unset($this->tools[$idx]);
2243
            }
2244
        }
2245
        return $this;
2246
    }
2247
2248
    /**
2249
     * @param string|AbstractBulkAction $bulkAction Pass name or class
2250
     * @return AbstractBulkAction|null
2251
     */
2252
    public function getBulkAction($bulkAction): ?AbstractBulkAction
2253
    {
2254
        if (is_object($bulkAction)) {
2255
            $bulkAction = get_class($bulkAction);
2256
        }
2257
        if (!is_string($bulkAction)) {
0 ignored issues
show
introduced by
The condition is_string($bulkAction) is always true.
Loading history...
2258
            throw new InvalidArgumentException('BulkAction must be an object or a class name');
2259
        }
2260
        foreach ($this->bulkActions as $ba) {
2261
            if ($ba->getName() == $bulkAction) {
2262
                return $ba;
2263
            }
2264
            if ($ba instanceof $bulkAction) {
2265
                return $ba;
2266
            }
2267
        }
2268
        return null;
2269
    }
2270
2271
    public function getBulkActions(): array
2272
    {
2273
        return $this->bulkActions;
2274
    }
2275
2276
    /**
2277
     * @param AbstractBulkAction[] $bulkActions
2278
     * @return self
2279
     */
2280
    public function setBulkActions(array $bulkActions): self
2281
    {
2282
        foreach ($bulkActions as $bulkAction) {
2283
            $bulkAction->setTabulatorGrid($this);
2284
        }
2285
        $this->bulkActions = $bulkActions;
2286
        return $this;
2287
    }
2288
2289
    public function addBulkAction(AbstractBulkAction $handler): self
2290
    {
2291
        $handler->setTabulatorGrid($this);
2292
2293
        $this->bulkActions[] = $handler;
2294
        return $this;
2295
    }
2296
2297
    public function removeBulkAction($bulkAction): self
2298
    {
2299
        if (is_object($bulkAction)) {
2300
            $bulkAction = get_class($bulkAction);
2301
        }
2302
        if (!is_string($bulkAction)) {
2303
            throw new InvalidArgumentException('Bulk action must be an object or a class name');
2304
        }
2305
        foreach ($this->bulkActions as $idx => $ba) {
2306
            if ($ba->getName() == $bulkAction) {
2307
                unset($this->bulkAction[$idx]);
0 ignored issues
show
Bug Best Practice introduced by
The property bulkAction does not exist on LeKoala\Tabulator\TabulatorGrid. Since you implemented __get, consider adding a @property annotation.
Loading history...
2308
            }
2309
            if ($ba instanceof $bulkAction) {
2310
                unset($this->bulkAction[$idx]);
2311
            }
2312
        }
2313
        return $this;
2314
    }
2315
2316
    public function getColumnDefault(string $opt)
2317
    {
2318
        return $this->columnDefaults[$opt] ?? null;
2319
    }
2320
2321
    public function setColumnDefault(string $opt, $value)
2322
    {
2323
        $this->columnDefaults[$opt] = $value;
2324
    }
2325
2326
    public function getColumnDefaults(): array
2327
    {
2328
        return $this->columnDefaults;
2329
    }
2330
2331
    public function setColumnDefaults(array $columnDefaults): self
2332
    {
2333
        $this->columnDefaults = $columnDefaults;
2334
        return $this;
2335
    }
2336
2337
    public function getListeners(): array
2338
    {
2339
        return $this->listeners;
2340
    }
2341
2342
    public function setListeners(array $listeners): self
2343
    {
2344
        $this->listeners = $listeners;
2345
        return $this;
2346
    }
2347
2348
    public function addListener(string $event, string $functionName): self
2349
    {
2350
        $this->listeners[$event] = $functionName;
2351
        return $this;
2352
    }
2353
2354
    public function removeListener(string $event): self
2355
    {
2356
        if (isset($this->listeners[$event])) {
2357
            unset($this->listeners[$event]);
2358
        }
2359
        return $this;
2360
    }
2361
2362
    public function getJsNamespaces(): array
2363
    {
2364
        return $this->jsNamespaces;
2365
    }
2366
2367
    public function setJsNamespaces(array $jsNamespaces): self
2368
    {
2369
        $this->jsNamespaces = $jsNamespaces;
2370
        return $this;
2371
    }
2372
2373
    public function registerJsNamespace(string $ns): self
2374
    {
2375
        $this->jsNamespaces[] = $ns;
2376
        return $this;
2377
    }
2378
2379
    public function unregisterJsNamespace(string $ns): self
2380
    {
2381
        $this->jsNamespaces = array_diff($this->jsNamespaces, [$ns]);
2382
        return $this;
2383
    }
2384
2385
    public function getLinksOptions(): array
2386
    {
2387
        return $this->linksOptions;
2388
    }
2389
2390
    public function setLinksOptions(array $linksOptions): self
2391
    {
2392
        $this->linksOptions = $linksOptions;
2393
        return $this;
2394
    }
2395
2396
    public function registerLinkOption(string $linksOption): self
2397
    {
2398
        $this->linksOptions[] = $linksOption;
2399
        return $this;
2400
    }
2401
2402
    public function unregisterLinkOption(string $linksOption): self
2403
    {
2404
        $this->linksOptions = array_diff($this->linksOptions, [$linksOption]);
2405
        return $this;
2406
    }
2407
2408
    /**
2409
     * Get the value of pageSize
2410
     */
2411
    public function getPageSize(): int
2412
    {
2413
        return $this->pageSize;
2414
    }
2415
2416
    /**
2417
     * Set the value of pageSize
2418
     *
2419
     * @param int $pageSize
2420
     */
2421
    public function setPageSize(int $pageSize): self
2422
    {
2423
        $this->pageSize = $pageSize;
2424
        return $this;
2425
    }
2426
2427
    /**
2428
     * Get the value of autoloadDataList
2429
     */
2430
    public function getAutoloadDataList(): bool
2431
    {
2432
        return $this->autoloadDataList;
2433
    }
2434
2435
    /**
2436
     * Set the value of autoloadDataList
2437
     *
2438
     * @param bool $autoloadDataList
2439
     */
2440
    public function setAutoloadDataList(bool $autoloadDataList): self
2441
    {
2442
        $this->autoloadDataList = $autoloadDataList;
2443
        return $this;
2444
    }
2445
2446
    /**
2447
     * Set the value of itemRequestClass
2448
     */
2449
    public function setItemRequestClass(string $itemRequestClass): self
2450
    {
2451
        $this->itemRequestClass = $itemRequestClass;
2452
        return $this;
2453
    }
2454
2455
    /**
2456
     * Get the value of lazyInit
2457
     */
2458
    public function getLazyInit(): bool
2459
    {
2460
        return $this->lazyInit;
2461
    }
2462
2463
    /**
2464
     * Set the value of lazyInit
2465
     */
2466
    public function setLazyInit(bool $lazyInit): self
2467
    {
2468
        $this->lazyInit = $lazyInit;
2469
        return $this;
2470
    }
2471
2472
    /**
2473
     * Get the value of rowClickTriggersAction
2474
     */
2475
    public function getRowClickTriggersAction(): bool
2476
    {
2477
        return $this->rowClickTriggersAction;
2478
    }
2479
2480
    /**
2481
     * Set the value of rowClickTriggersAction
2482
     */
2483
    public function setRowClickTriggersAction(bool $rowClickTriggersAction): self
2484
    {
2485
        $this->rowClickTriggersAction = $rowClickTriggersAction;
2486
        return $this;
2487
    }
2488
2489
    /**
2490
     * Get the value of controllerFunction
2491
     */
2492
    public function getControllerFunction(): string
2493
    {
2494
        if (!$this->controllerFunction) {
2495
            return $this->getName() ?? "TabulatorGrid";
2496
        }
2497
        return $this->controllerFunction;
2498
    }
2499
2500
    /**
2501
     * Set the value of controllerFunction
2502
     */
2503
    public function setControllerFunction(string $controllerFunction): self
2504
    {
2505
        $this->controllerFunction = $controllerFunction;
2506
        return $this;
2507
    }
2508
2509
    /**
2510
     * Get the value of useConfigProvider
2511
     */
2512
    public function getUseConfigProvider(): bool
2513
    {
2514
        return $this->useConfigProvider;
2515
    }
2516
2517
    /**
2518
     * Set the value of useConfigProvider
2519
     */
2520
    public function setUseConfigProvider(bool $useConfigProvider): self
2521
    {
2522
        $this->useConfigProvider = $useConfigProvider;
2523
        return $this;
2524
    }
2525
2526
    /**
2527
     * Get the value of useInitScript
2528
     */
2529
    public function getUseInitScript(): bool
2530
    {
2531
        return $this->useConfigProvider;
2532
    }
2533
2534
    /**
2535
     * Set the value of useInitScript
2536
     */
2537
    public function setUseInitScript(bool $useInitScript): self
2538
    {
2539
        $this->useInitScript = $useInitScript;
2540
        return $this;
2541
    }
2542
2543
    /**
2544
     * Get the value of editUrl
2545
     */
2546
    public function getEditUrl(): string
2547
    {
2548
        return $this->editUrl;
2549
    }
2550
2551
    /**
2552
     * Set the value of editUrl
2553
     */
2554
    public function setEditUrl(string $editUrl): self
2555
    {
2556
        $this->editUrl = $editUrl;
2557
        return $this;
2558
    }
2559
2560
    /**
2561
     * Get the value of moveUrl
2562
     */
2563
    public function getMoveUrl(): string
2564
    {
2565
        return $this->moveUrl;
2566
    }
2567
2568
    /**
2569
     * Set the value of moveUrl
2570
     */
2571
    public function setMoveUrl(string $moveUrl): self
2572
    {
2573
        $this->moveUrl = $moveUrl;
2574
        return $this;
2575
    }
2576
2577
    /**
2578
     * Get the value of bulkUrl
2579
     */
2580
    public function getBulkUrl(): string
2581
    {
2582
        return $this->bulkUrl;
2583
    }
2584
2585
    /**
2586
     * Set the value of bulkUrl
2587
     */
2588
    public function setBulkUrl(string $bulkUrl): self
2589
    {
2590
        $this->bulkUrl = $bulkUrl;
2591
        return $this;
2592
    }
2593
2594
    /**
2595
     * Get the value of globalSearch
2596
     */
2597
    public function getGlobalSearch(): bool
2598
    {
2599
        return $this->globalSearch;
2600
    }
2601
2602
    /**
2603
     * Set the value of globalSearch
2604
     *
2605
     * @param bool $globalSearch
2606
     */
2607
    public function setGlobalSearch($globalSearch): self
2608
    {
2609
        $this->globalSearch = $globalSearch;
2610
        return $this;
2611
    }
2612
2613
    /**
2614
     * Get the value of wildcardFields
2615
     */
2616
    public function getWildcardFields(): array
2617
    {
2618
        return $this->wildcardFields;
2619
    }
2620
2621
    /**
2622
     * Set the value of wildcardFields
2623
     *
2624
     * @param array $wildcardFields
2625
     */
2626
    public function setWildcardFields($wildcardFields): self
2627
    {
2628
        $this->wildcardFields = $wildcardFields;
2629
        return $this;
2630
    }
2631
2632
    /**
2633
     * Get the value of quickFilters
2634
     */
2635
    public function getQuickFilters(): array
2636
    {
2637
        return $this->quickFilters;
2638
    }
2639
2640
    /**
2641
     * Set the value of quickFilters
2642
     *
2643
     * @param array $quickFilters
2644
     */
2645
    public function setQuickFilters($quickFilters): self
2646
    {
2647
        $this->quickFilters = $quickFilters;
2648
        return $this;
2649
    }
2650
2651
    /**
2652
     * Get the value of groupLayout
2653
     */
2654
    public function getGroupLayout(): bool
2655
    {
2656
        return $this->groupLayout;
2657
    }
2658
2659
    /**
2660
     * Set the value of groupLayout
2661
     *
2662
     * @param bool $groupLayout
2663
     */
2664
    public function setGroupLayout($groupLayout): self
2665
    {
2666
        $this->groupLayout = $groupLayout;
2667
        return $this;
2668
    }
2669
2670
    /**
2671
     * Get the value of enableGridManipulation
2672
     */
2673
    public function getEnableGridManipulation(): bool
2674
    {
2675
        return $this->enableGridManipulation;
2676
    }
2677
2678
    /**
2679
     * Set the value of enableGridManipulation
2680
     *
2681
     * @param bool $enableGridManipulation
2682
     */
2683
    public function setEnableGridManipulation($enableGridManipulation): self
2684
    {
2685
        $this->enableGridManipulation = $enableGridManipulation;
2686
        return $this;
2687
    }
2688
}
2689