Passed
Push — master ( 98de9f...355f40 )
by Thomas
02:42
created

TabulatorGrid::setManageRelations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

554
            $this->addTool(self::POS_START, /** @scrutinizer ignore-call */ new TabulatorAddNewButton($this), self::TOOL_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...
555
        }
556
        $export = $opts['export'] ?? true;
557
        if (class_exists(\LeKoala\ExcelImportExport\ExcelImportExport::class) && $export) {
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...
558
            $this->addTool(self::POS_END, new TabulatorExportButton($this), self::TOOL_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

558
            $this->addTool(self::POS_END, /** @scrutinizer ignore-call */ new TabulatorExportButton($this), self::TOOL_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...
559
        }
560
561
        // - Custom actions are forwarded to the model itself
562
        if ($singl->hasMethod('tabulatorRowActions')) {
563
            $rowActions = $singl->tabulatorRowActions();
564
            if (!is_array($rowActions)) {
565
                throw new RuntimeException("tabulatorRowActions must return an array");
566
            }
567
            foreach ($rowActions as $key => $actionConfig) {
568
                $action = $actionConfig['action'] ?? $key;
569
                $url = $this->TempLink("item/{ID}/customAction/$action", false);
570
                $icon = $actionConfig['icon'] ?? "cog";
571
                $title = $actionConfig['title'] ?? "";
572
573
                $button = $this->makeButton($url, $icon, $title);
574
                if (!empty($actionConfig['ajax'])) {
575
                    $button['formatterParams']['ajax'] = true;
576
                }
577
                $this->addButtonFromArray("ui_customaction_$action", $button);
578
            }
579
        }
580
581
        $this->setRowClickTriggersAction(true);
582
    }
583
584
    public static function requirements(): void
585
    {
586
        $load_styles = self::config()->load_styles;
587
        $luxon_version = self::config()->luxon_version;
588
        $enable_luxon = self::config()->enable_luxon;
589
        $last_icon_version = self::config()->last_icon_version;
590
        $enable_last_icon = self::config()->enable_last_icon;
591
        $enable_js_modules = self::config()->enable_js_modules;
592
593
        $jsOpts = [];
594
        if ($enable_js_modules) {
595
            $jsOpts['type'] = 'module';
596
        }
597
598
        if ($luxon_version && $enable_luxon) {
599
            // Do not load as module or we would get undefined luxon global var
600
            Requirements::javascript("https://cdn.jsdelivr.net/npm/luxon@$luxon_version/build/global/luxon.min.js");
601
        }
602
        if ($last_icon_version && $enable_last_icon) {
603
            Requirements::css("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.css");
604
            // Do not load as module even if asked to ensure load speed
605
            Requirements::javascript("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.js");
606
        }
607
608
        Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js', $jsOpts);
609
        if ($load_styles) {
610
            Requirements::css('lekoala/silverstripe-tabulator:client/custom-tabulator.css');
611
            Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.min.js', $jsOpts);
612
        } else {
613
            // you must load th css yourself based on your preferences
614
            Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.raw.min.js', $jsOpts);
615
        }
616
    }
617
618
    public function setValue($value, $data = null)
619
    {
620
        // Allow set raw json as value
621
        if ($value && is_string($value) && strpos($value, '[') === 0) {
622
            $value = json_decode($value);
623
        }
624
        if ($value instanceof DataList) {
625
            $this->configureFromDataObject($value->dataClass());
626
        }
627
        return parent::setValue($value, $data);
628
    }
629
630
    public function Field($properties = [])
631
    {
632
        if (self::config()->enable_requirements) {
633
            self::requirements();
634
        }
635
636
        // Make sure we can use a standalone version of the field without a form
637
        // Function should match the name
638
        if (!$this->form) {
639
            $this->form = new Form(Controller::curr(), $this->getControllerFunction());
640
        }
641
642
        // Data attributes for our custom behaviour
643
        $this->setDataAttribute("row-click-triggers-action", $this->rowClickTriggersAction);
644
645
        $this->setDataAttribute("listeners", $this->listeners);
646
        if ($this->editUrl) {
647
            $url = $this->processLink($this->editUrl);
648
            $this->setDataAttribute("edit-url", $url);
649
        }
650
        if ($this->moveUrl) {
651
            $url = $this->processLink($this->moveUrl);
652
            $this->setDataAttribute("move-url", $url);
653
        }
654
        if (!empty($this->bulkActions)) {
655
            $url = $this->processLink($this->bulkUrl);
656
            $this->setDataAttribute("bulk-url", $url);
657
        }
658
659
        return parent::Field($properties);
660
    }
661
662
    public function ShowTools(): string
663
    {
664
        if (empty($this->tools)) {
665
            return '';
666
        }
667
        $html = '';
668
        $html .= '<div class="tabulator-tools">';
669
        $html .= '<div class="tabulator-tools-start">';
670
        foreach ($this->tools as $tool) {
671
            if ($tool['position'] != self::POS_START) {
672
                continue;
673
            }
674
            $html .= ($tool['tool'])->forTemplate();
675
        }
676
        $html .= '</div>';
677
        $html .= '<div class="tabulator-tools-end">';
678
        foreach ($this->tools as $tool) {
679
            if ($tool['position'] != self::POS_END) {
680
                continue;
681
            }
682
            $html .= ($tool['tool'])->forTemplate();
683
        }
684
        // Show bulk actions at the end
685
        if (!empty($this->bulkActions)) {
686
            $selectLabel = _t(__CLASS__ . ".BULKSELECT", "Select a bulk action");
687
            $confirmLabel = _t(__CLASS__ . ".BULKCONFIRM", "Go");
688
            $html .= "<select class=\"tabulator-bulk-select\">";
689
            $html .= "<option>" . $selectLabel . "</option>";
690
            foreach ($this->bulkActions as $bulkAction) {
691
                $v = $bulkAction->getName();
692
                $xhr = $bulkAction->getXhr();
693
                $destructive = $bulkAction->getDestructive();
694
                $html .= "<option value=\"$v\" data-xhr=\"$xhr\" data-destructive=\"$destructive\">" . $bulkAction->getLabel() . "</option>";
695
            }
696
            $html .= "</select>";
697
            $html .= "<button class=\"tabulator-bulk-confirm btn\">" . $confirmLabel . "</button>";
698
        }
699
        $html .= '</div>';
700
        $html .= '</div>';
701
        return $html;
702
    }
703
704
    public function JsonOptions(): string
705
    {
706
        $this->processLinks();
707
708
        $data = $this->list ?? [];
709
        if ($this->autoloadDataList && $data instanceof DataList) {
710
            $data = null;
711
        }
712
        $opts = $this->options;
713
        $opts['columnDefaults'] = $this->columnDefaults;
714
715
        if (empty($this->columns)) {
716
            $opts['autoColumns'] = true;
717
        } else {
718
            $opts['columns'] = array_values($this->columns);
719
        }
720
721
        if ($data && is_iterable($data)) {
722
            if ($data instanceof ArrayList) {
723
                $data = $data->toArray();
724
            } else {
725
                if (is_iterable($data) && !is_array($data)) {
726
                    $data = iterator_to_array($data);
727
                }
728
            }
729
            $opts['data'] = $data;
730
        }
731
732
        // i18n
733
        $locale = strtolower(str_replace('_', '-', i18n::get_locale()));
734
        $paginationTranslations = [
735
            "first" => _t("TabulatorPagination.first", "First"),
736
            "first_title" =>  _t("TabulatorPagination.first_title", "First Page"),
737
            "last" =>  _t("TabulatorPagination.last", "Last"),
738
            "last_title" => _t("TabulatorPagination.last_title", "Last Page"),
739
            "prev" => _t("TabulatorPagination.prev", "Previous"),
740
            "prev_title" =>  _t("TabulatorPagination.prev_title", "Previous Page"),
741
            "next" => _t("TabulatorPagination.next", "Next"),
742
            "next_title" =>  _t("TabulatorPagination.next_title", "Next Page"),
743
            "all" =>  _t("TabulatorPagination.all", "All"),
744
        ];
745
        $dataTranslations = [
746
            "loading" => _t("TabulatorData.loading", "Loading"),
747
            "error" => _t("TabulatorData.error", "Error"),
748
        ];
749
        $groupsTranslations = [
750
            "item" => _t("TabulatorGroups.item", "Item"),
751
            "items" => _t("TabulatorGroups.items", "Items"),
752
        ];
753
        $headerFiltersTranslations = [
754
            "default" => _t("TabulatorHeaderFilters.default", "filter column..."),
755
        ];
756
        $bulkActionsTranslations = [
757
            "no_action" => _t("TabulatorBulkActions.no_action", "Please select an action"),
758
            "no_records" => _t("TabulatorBulkActions.no_records", "Please select a record"),
759
            "destructive" => _t("TabulatorBulkActions.destructive", "Confirm destructive action ?"),
760
        ];
761
        $translations = [
762
            'data' => $dataTranslations,
763
            'groups' => $groupsTranslations,
764
            'pagination' => $paginationTranslations,
765
            'headerFilters' => $headerFiltersTranslations,
766
            'bulkActions' => $bulkActionsTranslations,
767
        ];
768
        $opts['locale'] = $locale;
769
        $opts['langs'] = [
770
            $locale => $translations
771
        ];
772
773
        // Apply state
774
        // TODO: finalize persistence on the client side instead of this when using TabID
775
        $state = $this->getState();
776
        if ($state) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $state of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
777
            if (!empty($state['filter'])) {
778
                // @link https://tabulator.info/docs/5.5/filter#initial
779
                // We need to split between global filters and header filters
780
                $allFilters = $state['filter'] ?? [];
781
                $globalFilters = [];
782
                $headerFilters = [];
783
                foreach ($allFilters as $allFilter) {
784
                    if (strpos($allFilter['field'], '__') === 0) {
785
                        $globalFilters[] = $allFilter;
786
                    } else {
787
                        $headerFilters[] = $allFilter;
788
                    }
789
                }
790
                $opts['initialFilter'] = $globalFilters;
791
                $opts['initialHeaderFilter'] = $headerFilters;
792
            }
793
            if (!empty($state['sort'])) {
794
                // @link https://tabulator.info/docs/5.5/sort#initial
795
                $opts['initialSort'] = $state['sort'];
796
            }
797
798
            // Restore state from server
799
            $opts['_state'] = $state;
800
        }
801
802
        if ($this->enableGridManipulation) {
803
            // $opts['renderVertical'] = 'basic';
804
        }
805
806
        // Add our extension initCallback
807
        $opts['_initCallback'] = ['__fn' => self::JS_INIT_CALLBACK];
808
        $opts['_configCallback'] = ['__fn' => self::JS_CONFIG_CALLBACK];
809
810
        unset($opts['height']);
811
        $json = json_encode($opts);
812
813
        // Escape '
814
        $json = str_replace("'", '&#39;', $json);
815
816
        return $json;
817
    }
818
819
    /**
820
     * @param Controller $controller
821
     * @return CompatLayerInterface
822
     */
823
    public function getCompatLayer(Controller $controller = null)
824
    {
825
        if ($controller === null) {
826
            $controller = Controller::curr();
827
        }
828
        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...
829
            return new SilverstripeAdminCompat();
830
        }
831
        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...
832
            return new AdminiCompat();
833
        }
834
    }
835
836
    public function getAttributes()
837
    {
838
        $attrs = parent::getAttributes();
839
        unset($attrs['type']);
840
        unset($attrs['name']);
841
        unset($attrs['value']);
842
        return $attrs;
843
    }
844
845
    public function getOption(string $k)
846
    {
847
        return $this->options[$k] ?? null;
848
    }
849
850
    public function setOption(string $k, $v): self
851
    {
852
        $this->options[$k] = $v;
853
        return $this;
854
    }
855
856
    public function getRowHeight(): int
857
    {
858
        return $this->getOption('rowHeight');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getOption('rowHeight') could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
859
    }
860
861
    /**
862
     * Prevent row height automatic computation
863
     * @link https://tabulator.info/docs/5.5/layout#height-row
864
     */
865
    public function setRowHeight(int $v): self
866
    {
867
        $this->setOption('rowHeight', $v);
868
        return $this;
869
    }
870
871
    public function makeHeadersSticky(): self
872
    {
873
        // note: we could also use the "sticky" attribute on the custom element
874
        $this->addExtraClass("tabulator-sticky");
875
        return $this;
876
    }
877
878
    public function setRemoteSource(string $url, array $extraParams = [], bool $dataResponse = false): self
879
    {
880
        $this->setOption("ajaxURL", $url); //set url for ajax request
881
        $params = array_merge([
882
            'SecurityID' => SecurityToken::getSecurityID()
883
        ], $extraParams);
884
        $this->setOption("ajaxParams", $params);
885
        // Accept response where data is nested under the data key
886
        if ($dataResponse) {
887
            $this->setOption("ajaxResponse", ['__fn' => self::JS_DATA_AJAX_RESPONSE]);
888
        }
889
        return $this;
890
    }
891
892
    /**
893
     * @link http://www.tabulator.info/docs/5.5/page#remote
894
     * @param string $url
895
     * @param array $params
896
     * @param integer $pageSize
897
     * @param integer $initialPage
898
     */
899
    public function setRemotePagination(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1): self
900
    {
901
        $this->setOption("pagination", true); //enable pagination
902
        $this->setOption("paginationMode", 'remote'); //enable remote pagination
903
        $this->setRemoteSource($url, $params);
904
        if (!$pageSize) {
905
            $pageSize = $this->pageSize;
906
        } else {
907
            $this->pageSize = $pageSize;
908
        }
909
        $this->setOption("paginationSize", $pageSize);
910
        $this->setOption("paginationInitialPage", $initialPage);
911
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/5.5/page#counter
912
        return $this;
913
    }
914
915
    public function wizardRemotePagination(int $pageSize = 0, int $initialPage = 1, array $params = []): self
916
    {
917
        $this->setRemotePagination($this->TempLink('load', false), $params, $pageSize, $initialPage);
918
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/5.5/sort#ajax-sort
919
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/5.5/filter#ajax-filter
920
        return $this;
921
    }
922
923
    public function setProgressiveLoad(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0): self
924
    {
925
        $this->setOption("ajaxURL", $url);
926
        if (!empty($params)) {
927
            $this->setOption("ajaxParams", $params);
928
        }
929
        $this->setOption("progressiveLoad", $mode);
930
        if ($scrollMargin > 0) {
931
            $this->setOption("progressiveLoadScrollMargin", $scrollMargin);
932
        }
933
        if (!$pageSize) {
934
            $pageSize = $this->pageSize;
935
        } else {
936
            $this->pageSize = $pageSize;
937
        }
938
        $this->setOption("paginationSize", $pageSize);
939
        $this->setOption("paginationInitialPage", $initialPage);
940
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/5.5/page#counter
941
        return $this;
942
    }
943
944
    public function wizardProgressiveLoad(int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0, array $extraParams = []): self
945
    {
946
        $params = array_merge([
947
            'SecurityID' => SecurityToken::getSecurityID()
948
        ], $extraParams);
949
        $this->setProgressiveLoad($this->TempLink('load', false), $params, $pageSize, $initialPage, $mode, $scrollMargin);
950
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/5.5/sort#ajax-sort
951
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/5.5/filter#ajax-filter
952
        return $this;
953
    }
954
955
    /**
956
     * @link https://tabulator.info/docs/5.5/layout#responsive
957
     * @param boolean $startOpen
958
     * @param string $mode collapse|hide|flexCollapse
959
     * @return self
960
     */
961
    public function wizardResponsiveCollapse(bool $startOpen = false, string $mode = "collapse"): self
962
    {
963
        $this->setOption("responsiveLayout", $mode);
964
        $this->setOption("responsiveLayoutCollapseStartOpen", $startOpen);
965
        if ($mode != "hide") {
966
            $this->columns = array_merge([
967
                'ui_responsive_collapse' => [
968
                    "cssClass" => 'tabulator-cell-btn',
969
                    'formatter' => 'responsiveCollapse',
970
                    'headerSort' => false,
971
                    'width' => 40,
972
                ]
973
            ], $this->columns);
974
        }
975
        return $this;
976
    }
977
978
    public function wizardDataTree(bool $startExpanded = false, bool $filter = false, bool $sort = false, string $el = null): self
979
    {
980
        $this->setOption("dataTree", true);
981
        $this->setOption("dataTreeStartExpanded", $startExpanded);
982
        $this->setOption("dataTreeFilter", $filter);
983
        $this->setOption("dataTreeSort", $sort);
984
        if ($el) {
985
            $this->setOption("dataTreeElementColumn", $el);
986
        }
987
        return $this;
988
    }
989
990
    /**
991
     * @param array $actions An array of bulk actions, that can extend the abstract one or use the generic with callbable
992
     * @return self
993
     */
994
    public function wizardSelectable(array $actions = []): self
995
    {
996
        $this->columns = array_merge([
997
            'ui_selectable' => [
998
                "hozAlign" => 'center',
999
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector',
1000
                'formatter' => 'rowSelection',
1001
                'titleFormatter' => 'rowSelection',
1002
                'width' => 40,
1003
                'maxWidth' => 40,
1004
                "headerSort" => false,
1005
            ]
1006
        ], $this->columns);
1007
        $this->setBulkActions($actions);
1008
        return $this;
1009
    }
1010
1011
    public function wizardMoveable(string $callback = "SSTabulator.rowMoved", $field = "Sort"): self
1012
    {
1013
        $this->moveUrl = $this->TempLink("item/{ID}/ajaxMove", false);
1014
        $this->setOption("movableRows", true);
1015
        $this->addListener("rowMoved", $callback);
1016
        $this->columns = array_merge([
1017
            'ui_move' => [
1018
                "hozAlign" => 'center',
1019
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector tabulator-ui-sort',
1020
                'rowHandle' => true,
1021
                'formatter' => 'handle',
1022
                'headerSort' => false,
1023
                'frozen' => true,
1024
                'width' => 40,
1025
                'maxWidth' => 40,
1026
            ],
1027
            // We need a hidden sort column
1028
            self::UI_SORT => [
1029
                "field" => $field,
1030
                'visible' => false,
1031
            ],
1032
        ], $this->columns);
1033
        return $this;
1034
    }
1035
1036
    /**
1037
     * @param string $field
1038
     * @param string $toggleElement arrow|header|false (header by default)
1039
     * @param boolean $isBool
1040
     * @return void
1041
     */
1042
    public function wizardGroupBy(string $field, string $toggleElement = 'header', bool $isBool = false)
1043
    {
1044
        $this->setOption("groupBy", $field);
1045
        $this->setOption("groupToggleElement", $toggleElement);
1046
        if ($isBool) {
1047
            $this->setOption("groupHeader", ['_fn' => self::JS_BOOL_GROUP_HEADER]);
1048
        }
1049
    }
1050
1051
    /**
1052
     * @param HTTPRequest $request
1053
     * @return HTTPResponse
1054
     */
1055
    public function handleItem($request)
1056
    {
1057
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1058
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1059
        $requestHandler = $this->getForm()->getController();
1060
        $record = $this->getRecordFromRequest($request);
1061
        if (!$record) {
1062
            return $requestHandler->httpError(404, 'That record was not found');
1063
        }
1064
        $handler = $this->getItemRequestHandler($record, $requestHandler);
1065
        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...
1066
    }
1067
1068
    /**
1069
     * @param HTTPRequest $request
1070
     * @return HTTPResponse
1071
     */
1072
    public function handleTool($request)
1073
    {
1074
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1075
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1076
        $requestHandler = $this->getForm()->getController();
1077
        $tool = $this->getToolFromRequest($request);
1078
        if (!$tool) {
1079
            return $requestHandler->httpError(404, 'That tool was not found');
1080
        }
1081
        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...
1082
    }
1083
1084
    /**
1085
     * @param HTTPRequest $request
1086
     * @return HTTPResponse
1087
     */
1088
    public function handleBulkAction($request)
1089
    {
1090
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1091
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1092
        $requestHandler = $this->getForm()->getController();
1093
        $bulkAction = $this->getBulkActionFromRequest($request);
1094
        if (!$bulkAction) {
1095
            return $requestHandler->httpError(404, 'That bulk action was not found');
1096
        }
1097
        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...
1098
    }
1099
1100
    /**
1101
     * @return string name of {@see TabulatorGrid_ItemRequest} subclass
1102
     */
1103
    public function getItemRequestClass(): string
1104
    {
1105
        if ($this->itemRequestClass) {
1106
            return $this->itemRequestClass;
1107
        } elseif (ClassInfo::exists(static::class . '_ItemRequest')) {
1108
            return static::class . '_ItemRequest';
1109
        }
1110
        return TabulatorGrid_ItemRequest::class;
1111
    }
1112
1113
    /**
1114
     * Build a request handler for the given record
1115
     *
1116
     * @param DataObject $record
1117
     * @param RequestHandler $requestHandler
1118
     * @return TabulatorGrid_ItemRequest
1119
     */
1120
    protected function getItemRequestHandler($record, $requestHandler)
1121
    {
1122
        $class = $this->getItemRequestClass();
1123
        $assignedClass = $this->itemRequestClass;
1124
        $this->extend('updateItemRequestClass', $class, $record, $requestHandler, $assignedClass);
1125
        /** @var TabulatorGrid_ItemRequest $handler */
1126
        $handler = Injector::inst()->createWithArgs(
1127
            $class,
1128
            [$this, $record, $requestHandler]
1129
        );
1130
        if ($template = $this->getTemplate()) {
1131
            $handler->setTemplate($template);
1132
        }
1133
        $this->extend('updateItemRequestHandler', $handler);
1134
        return $handler;
1135
    }
1136
1137
    public function getStateKey(string $TabID = null)
1138
    {
1139
        $nested = [];
1140
        $form = $this->getForm();
1141
        $scope = $this->modelClass ? str_replace('_', '\\', $this->modelClass) :  "default";
1142
        if ($form) {
0 ignored issues
show
introduced by
$form is of type SilverStripe\Forms\Form, thus it always evaluated to true.
Loading history...
1143
            $controller = $form->getController();
1144
1145
            // We are in a nested form, track by id since each records needs it own state
1146
            while ($controller instanceof TabulatorGrid_ItemRequest) {
1147
                $record = $controller->getRecord();
1148
                $nested[str_replace('_', '\\', get_class($record))] = $record->ID;
1149
1150
                // Move to parent controller
1151
                $controller = $controller->getController();
1152
            }
1153
1154
            // Scope by top controller class
1155
            $scope = str_replace('_', '\\', get_class($controller));
1156
        }
1157
1158
        $baseKey = 'TabulatorState';
1159
        if ($TabID) {
1160
            $baseKey .= '_' . $TabID;
1161
        }
1162
        $name = $this->getName();
1163
        $key = "$baseKey.$scope.$name";
1164
        foreach ($nested as $k => $v) {
1165
            $key .= "$k.$v";
1166
        }
1167
        return $key;
1168
    }
1169
1170
    /**
1171
     * @param HTTPRequest|null $request
1172
     * @return array{'page': int, 'limit': int, 'sort': array, 'filter': array}
1173
     */
1174
    public function getState(HTTPRequest $request = null)
1175
    {
1176
        if ($request === null) {
1177
            $request = Controller::curr()->getRequest();
1178
        }
1179
        $TabID = $request->requestVar('TabID') ?? null;
1180
        $stateKey = $this->getStateKey($TabID);
1181
        $state = $request->getSession()->get($stateKey);
1182
        return $state ?? [
1183
            'page' => 1,
1184
            'limit' => $this->pageSize,
1185
            'sort' => [],
1186
            'filter' => [],
1187
        ];
1188
    }
1189
1190
    public function setState(HTTPRequest $request, $state)
1191
    {
1192
        $TabID = $request->requestVar('TabID') ?? null;
1193
        $stateKey = $this->getStateKey($TabID);
1194
        $request->getSession()->set($stateKey, $state);
1195
        // If we are in a new controller, we can clear other states
1196
        // Note: this would break tabbed navigation if you try to open multiple tabs, see below for more info
1197
        // @link https://github.com/silverstripe/silverstripe-framework/issues/9556
1198
        $matches = [];
1199
        preg_match_all('/\.(.*?)\./', $stateKey, $matches);
1200
        $scope = $matches[1][0] ?? null;
1201
        if ($scope) {
1202
            self::clearAllStates($scope);
1203
        }
1204
    }
1205
1206
    public function clearState(HTTPRequest $request)
1207
    {
1208
        $TabID = $request->requestVar('TabID') ?? null;
1209
        $stateKey = $this->getStateKey($TabID);
1210
        $request->getSession()->clear($stateKey);
1211
    }
1212
1213
    public static function clearAllStates(string $exceptScope = null, string $TabID = null)
1214
    {
1215
        $request = Controller::curr()->getRequest();
1216
        $baseKey = 'TabulatorState';
1217
        if ($TabID) {
1218
            $baseKey .= '_' . $TabID;
1219
        }
1220
        $allStates = $request->getSession()->get($baseKey);
1221
        if (!$allStates) {
1222
            return;
1223
        }
1224
        foreach ($allStates as $scope => $data) {
1225
            if ($exceptScope && $scope == $exceptScope) {
1226
                continue;
1227
            }
1228
            $request->getSession()->clear("TabulatorState.$scope");
1229
        }
1230
    }
1231
1232
    public function StateValue($key, $field): ?string
1233
    {
1234
        $state = $this->getState();
1235
        $arr = $state[$key] ?? [];
1236
        foreach ($arr as $s) {
1237
            if ($s['field'] === $field) {
1238
                return $s['value'];
1239
            }
1240
        }
1241
        return null;
1242
    }
1243
1244
    /**
1245
     * Provides autocomplete lists
1246
     *
1247
     * @param HTTPRequest $request
1248
     * @return HTTPResponse
1249
     */
1250
    public function autocomplete(HTTPRequest $request)
1251
    {
1252
        if ($this->isDisabled() || $this->isReadonly()) {
1253
            return $this->httpError(403);
1254
        }
1255
        $SecurityID = $request->getVar('SecurityID');
1256
        if (!SecurityToken::inst()->check($SecurityID)) {
1257
            return $this->httpError(404, "Invalid SecurityID");
1258
        }
1259
1260
        $name = $request->getVar("Column");
1261
        $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

1261
        $col = $this->getColumn(/** @scrutinizer ignore-type */ $name);
Loading history...
1262
        if (!$col) {
1263
            return $this->httpError(403, "Invalid column");
1264
        }
1265
1266
        // Don't use % term as it prevents use of indexes
1267
        $term = $request->getVar('term') . '%';
1268
        $term = str_replace(' ', '%', $term);
1269
1270
        $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

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

1514
            throw new RuntimeException("Value is not a ArrayList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1515
        }
1516
        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...
1517
    }
1518
1519
    public function hasDataList(): bool
1520
    {
1521
        return $this->list instanceof DataList;
1522
    }
1523
1524
    /**
1525
     * A properly typed on which you can call byID
1526
     * @return ArrayList|DataList
1527
     */
1528
    public function getByIDList()
1529
    {
1530
        return $this->list;
1531
    }
1532
1533
    public function hasByIDList(): bool
1534
    {
1535
        return $this->hasDataList() || $this->hasArrayList();
1536
    }
1537
1538
    public function getDataList(): DataList
1539
    {
1540
        if (!$this->list instanceof DataList) {
1541
            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

1541
            throw new RuntimeException("Value is not a DataList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1542
        }
1543
        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...
1544
    }
1545
1546
    public function getManipulatedData(int $limit, int $offset, array $sort = null, array $filter = null): array
1547
    {
1548
        if (!$this->hasDataList()) {
1549
            $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

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