Passed
Push — master ( 5e1cee...2b0ca1 )
by Thomas
03:06
created

TabulatorGrid::processLink()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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

563
            $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...
564
        }
565
        $export = $opts['export'] ?? true;
566
        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...
567
            $xlsxExportButton = new TabulatorExportButton($this);
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

567
            $xlsxExportButton = /** @scrutinizer ignore-call */ new TabulatorExportButton($this);

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...
568
            $this->addTool(self::POS_END, $xlsxExportButton, self::TOOL_EXPORT);
569
            $csvExportButton = new TabulatorExportButton($this);
570
            $csvExportButton->setExportFormat('csv');
571
            $this->addTool(self::POS_END, $csvExportButton, self::TOOL_EXPORT_CSV);
572
        }
573
574
        // - Custom actions are forwarded to the model itself
575
        if ($singl->hasMethod('tabulatorRowActions')) {
576
            $rowActions = $singl->tabulatorRowActions();
577
            if (!is_array($rowActions)) {
578
                throw new RuntimeException("tabulatorRowActions must return an array");
579
            }
580
            foreach ($rowActions as $key => $actionConfig) {
581
                $action = $actionConfig['action'] ?? $key;
582
                $url = $this->TempLink("item/{ID}/customAction/$action", false);
583
                $icon = $actionConfig['icon'] ?? "cog";
584
                $title = $actionConfig['title'] ?? "";
585
586
                $button = $this->makeButton($url, $icon, $title);
587
                if (!empty($actionConfig['ajax'])) {
588
                    $button['formatterParams']['ajax'] = true;
589
                }
590
                $this->addButtonFromArray("ui_customaction_$action", $button);
591
            }
592
        }
593
594
        $this->setRowClickTriggersAction(true);
595
    }
596
597
    public static function requirements(): void
598
    {
599
        $load_styles = self::config()->load_styles;
600
        $luxon_version = self::config()->luxon_version;
601
        $enable_luxon = self::config()->enable_luxon;
602
        $last_icon_version = self::config()->last_icon_version;
603
        $enable_last_icon = self::config()->enable_last_icon;
604
        $enable_js_modules = self::config()->enable_js_modules;
605
606
        $jsOpts = [];
607
        if ($enable_js_modules) {
608
            $jsOpts['type'] = 'module';
609
        }
610
611
        if ($luxon_version && $enable_luxon) {
612
            // Do not load as module or we would get undefined luxon global var
613
            Requirements::javascript("https://cdn.jsdelivr.net/npm/luxon@$luxon_version/build/global/luxon.min.js");
614
        }
615
        if ($last_icon_version && $enable_last_icon) {
616
            Requirements::css("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.css");
617
            // Do not load as module even if asked to ensure load speed
618
            Requirements::javascript("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.js");
619
        }
620
621
        Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js', $jsOpts);
622
        if ($load_styles) {
623
            Requirements::css('lekoala/silverstripe-tabulator:client/custom-tabulator.css');
624
            Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.min.js', $jsOpts);
625
        } else {
626
            // you must load th css yourself based on your preferences
627
            Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.raw.min.js', $jsOpts);
628
        }
629
    }
630
631
    public function setValue($value, $data = null)
632
    {
633
        // Allow set raw json as value
634
        if ($value && is_string($value) && strpos($value, '[') === 0) {
635
            $value = json_decode($value);
636
        }
637
        if ($value instanceof DataList) {
638
            $this->configureFromDataObject($value->dataClass());
639
        }
640
        return parent::setValue($value, $data);
641
    }
642
643
    public function Field($properties = [])
644
    {
645
        if (self::config()->enable_requirements) {
646
            self::requirements();
647
        }
648
649
        // Make sure we can use a standalone version of the field without a form
650
        // Function should match the name
651
        if (!$this->form) {
652
            $this->form = new Form(Controller::curr(), $this->getControllerFunction());
653
        }
654
655
        // Data attributes for our custom behaviour
656
        $this->setDataAttribute("row-click-triggers-action", $this->rowClickTriggersAction);
657
658
        $this->setDataAttribute("listeners", $this->listeners);
659
        if ($this->editUrl) {
660
            $url = $this->processLink($this->editUrl);
661
            $this->setDataAttribute("edit-url", $url);
662
        }
663
        if ($this->moveUrl) {
664
            $url = $this->processLink($this->moveUrl);
665
            $this->setDataAttribute("move-url", $url);
666
        }
667
        if (!empty($this->bulkActions)) {
668
            $url = $this->processLink($this->bulkUrl);
669
            $this->setDataAttribute("bulk-url", $url);
670
        }
671
672
        return parent::Field($properties);
673
    }
674
675
    public function ShowTools(): string
676
    {
677
        if (empty($this->tools)) {
678
            return '';
679
        }
680
        $html = '';
681
        $html .= '<div class="tabulator-tools">';
682
        $html .= '<div class="tabulator-tools-start">';
683
        foreach ($this->tools as $tool) {
684
            if ($tool['position'] != self::POS_START) {
685
                continue;
686
            }
687
            $html .= ($tool['tool'])->forTemplate();
688
        }
689
        $html .= '</div>';
690
        $html .= '<div class="tabulator-tools-end">';
691
        foreach ($this->tools as $tool) {
692
            if ($tool['position'] != self::POS_END) {
693
                continue;
694
            }
695
            $html .= ($tool['tool'])->forTemplate();
696
        }
697
        // Show bulk actions at the end
698
        if (!empty($this->bulkActions)) {
699
            $selectLabel = _t(__CLASS__ . ".BULKSELECT", "Select a bulk action");
700
            $confirmLabel = _t(__CLASS__ . ".BULKCONFIRM", "Go");
701
            $html .= "<select class=\"tabulator-bulk-select\">";
702
            $html .= "<option>" . $selectLabel . "</option>";
703
            foreach ($this->bulkActions as $bulkAction) {
704
                $v = $bulkAction->getName();
705
                $xhr = $bulkAction->getXhr();
706
                $destructive = $bulkAction->getDestructive();
707
                $html .= "<option value=\"$v\" data-xhr=\"$xhr\" data-destructive=\"$destructive\">" . $bulkAction->getLabel() . "</option>";
708
            }
709
            $html .= "</select>";
710
            $html .= "<button class=\"tabulator-bulk-confirm btn\">" . $confirmLabel . "</button>";
711
        }
712
        $html .= '</div>';
713
        $html .= '</div>';
714
        return $html;
715
    }
716
717
    public function JsonOptions(): string
718
    {
719
        $this->processLinks();
720
721
        $data = $this->list ?? [];
722
        if ($this->autoloadDataList && $data instanceof DataList) {
723
            $data = null;
724
        }
725
        $opts = $this->options;
726
        $opts['columnDefaults'] = $this->columnDefaults;
727
728
        if (empty($this->columns)) {
729
            $opts['autoColumns'] = true;
730
        } else {
731
            $opts['columns'] = array_values($this->columns);
732
        }
733
734
        if ($data && is_iterable($data)) {
735
            if ($data instanceof ArrayList) {
736
                $data = $data->toArray();
737
            } else {
738
                if (is_iterable($data) && !is_array($data)) {
739
                    $data = iterator_to_array($data);
740
                }
741
            }
742
            $opts['data'] = $data;
743
        }
744
745
        // i18n
746
        $locale = strtolower(str_replace('_', '-', i18n::get_locale()));
747
        $paginationTranslations = [
748
            "first" => _t("TabulatorPagination.first", "First"),
749
            "first_title" =>  _t("TabulatorPagination.first_title", "First Page"),
750
            "last" =>  _t("TabulatorPagination.last", "Last"),
751
            "last_title" => _t("TabulatorPagination.last_title", "Last Page"),
752
            "prev" => _t("TabulatorPagination.prev", "Previous"),
753
            "prev_title" =>  _t("TabulatorPagination.prev_title", "Previous Page"),
754
            "next" => _t("TabulatorPagination.next", "Next"),
755
            "next_title" =>  _t("TabulatorPagination.next_title", "Next Page"),
756
            "all" =>  _t("TabulatorPagination.all", "All"),
757
        ];
758
        $dataTranslations = [
759
            "loading" => _t("TabulatorData.loading", "Loading"),
760
            "error" => _t("TabulatorData.error", "Error"),
761
        ];
762
        $groupsTranslations = [
763
            "item" => _t("TabulatorGroups.item", "Item"),
764
            "items" => _t("TabulatorGroups.items", "Items"),
765
        ];
766
        $headerFiltersTranslations = [
767
            "default" => _t("TabulatorHeaderFilters.default", "filter column..."),
768
        ];
769
        $bulkActionsTranslations = [
770
            "no_action" => _t("TabulatorBulkActions.no_action", "Please select an action"),
771
            "no_records" => _t("TabulatorBulkActions.no_records", "Please select a record"),
772
            "destructive" => _t("TabulatorBulkActions.destructive", "Confirm destructive action ?"),
773
        ];
774
        $translations = [
775
            'data' => $dataTranslations,
776
            'groups' => $groupsTranslations,
777
            'pagination' => $paginationTranslations,
778
            'headerFilters' => $headerFiltersTranslations,
779
            'bulkActions' => $bulkActionsTranslations,
780
        ];
781
        $opts['locale'] = $locale;
782
        $opts['langs'] = [
783
            $locale => $translations
784
        ];
785
786
        // Apply state
787
        // TODO: finalize persistence on the client side instead of this when using TabID
788
        $state = $this->getState();
789
        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...
790
            if (!empty($state['filter'])) {
791
                // @link https://tabulator.info/docs/6.2/filter#initial
792
                // We need to split between global filters and header filters
793
                $allFilters = $state['filter'] ?? [];
794
                $globalFilters = [];
795
                $headerFilters = [];
796
                foreach ($allFilters as $allFilter) {
797
                    if (strpos($allFilter['field'], '__') === 0) {
798
                        $globalFilters[] = $allFilter;
799
                    } else {
800
                        $headerFilters[] = $allFilter;
801
                    }
802
                }
803
                $opts['initialFilter'] = $globalFilters;
804
                $opts['initialHeaderFilter'] = $headerFilters;
805
            }
806
            if (!empty($state['sort'])) {
807
                // @link https://tabulator.info/docs/6.2/sort#initial
808
                $opts['initialSort'] = $state['sort'];
809
            }
810
811
            // Restore state from server
812
            $opts['_state'] = $state;
813
        }
814
815
        if ($this->enableGridManipulation) {
816
            // $opts['renderVertical'] = 'basic';
817
        }
818
819
        // Add our extension initCallback
820
        $opts['_initCallback'] = ['__fn' => self::JS_INIT_CALLBACK];
821
        $opts['_configCallback'] = ['__fn' => self::JS_CONFIG_CALLBACK];
822
823
        unset($opts['height']);
824
        $json = json_encode($opts);
825
826
        // Escape '
827
        $json = str_replace("'", '&#39;', $json);
828
829
        return $json;
830
    }
831
832
    /**
833
     * @param Controller $controller
834
     * @return CompatLayerInterface
835
     */
836
    public function getCompatLayer(Controller $controller = null)
837
    {
838
        if ($controller === null) {
839
            $controller = Controller::curr();
840
        }
841
        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...
842
            return new SilverstripeAdminCompat();
843
        }
844
        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...
845
            return new AdminiCompat();
846
        }
847
    }
848
849
    public function getAttributes()
850
    {
851
        $attrs = parent::getAttributes();
852
        unset($attrs['type']);
853
        unset($attrs['name']);
854
        unset($attrs['value']);
855
        return $attrs;
856
    }
857
858
    public function getOption(string $k)
859
    {
860
        return $this->options[$k] ?? null;
861
    }
862
863
    public function setOption(string $k, $v): self
864
    {
865
        $this->options[$k] = $v;
866
        return $this;
867
    }
868
869
    public function getRowHeight(): int
870
    {
871
        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...
872
    }
873
874
    /**
875
     * Prevent row height automatic computation
876
     * @link https://tabulator.info/docs/6.2/layout#height-row
877
     */
878
    public function setRowHeight(int $v): self
879
    {
880
        $this->setOption('rowHeight', $v);
881
        return $this;
882
    }
883
884
    public function makeHeadersSticky(): self
885
    {
886
        // note: we could also use the "sticky" attribute on the custom element
887
        $this->addExtraClass("tabulator-sticky");
888
        return $this;
889
    }
890
891
    public function setRemoteSource(string $url, array $extraParams = [], bool $dataResponse = false): self
892
    {
893
        $this->setOption("ajaxURL", $url); //set url for ajax request
894
        $params = array_merge([
895
            'SecurityID' => SecurityToken::getSecurityID()
896
        ], $extraParams);
897
        $this->setOption("ajaxParams", $params);
898
        // Accept response where data is nested under the data key
899
        if ($dataResponse) {
900
            $this->setOption("ajaxResponse", ['__fn' => self::JS_DATA_AJAX_RESPONSE]);
901
        }
902
        return $this;
903
    }
904
905
    /**
906
     * @link http://www.tabulator.info/docs/6.2/page#remote
907
     * @param string $url
908
     * @param array $params
909
     * @param integer $pageSize
910
     * @param integer $initialPage
911
     */
912
    public function setRemotePagination(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1): self
913
    {
914
        $this->setOption("pagination", true); //enable pagination
915
        $this->setOption("paginationMode", 'remote'); //enable remote pagination
916
        $this->setRemoteSource($url, $params);
917
        if (!$pageSize) {
918
            $pageSize = $this->pageSize;
919
        } else {
920
            $this->pageSize = $pageSize;
921
        }
922
        $this->setOption("paginationSize", $pageSize);
923
        $this->setOption("paginationInitialPage", $initialPage);
924
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/6.2/page#counter
925
        return $this;
926
    }
927
928
    public function wizardRemotePagination(int $pageSize = 0, int $initialPage = 1, array $params = []): self
929
    {
930
        $this->setRemotePagination($this->TempLink('load', false), $params, $pageSize, $initialPage);
931
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/6.2/sort#ajax-sort
932
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/6.2/filter#ajax-filter
933
        return $this;
934
    }
935
936
    public function setProgressiveLoad(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0): self
937
    {
938
        $this->setOption("ajaxURL", $url);
939
        if (!empty($params)) {
940
            $this->setOption("ajaxParams", $params);
941
        }
942
        $this->setOption("progressiveLoad", $mode);
943
        if ($scrollMargin > 0) {
944
            $this->setOption("progressiveLoadScrollMargin", $scrollMargin);
945
        }
946
        if (!$pageSize) {
947
            $pageSize = $this->pageSize;
948
        } else {
949
            $this->pageSize = $pageSize;
950
        }
951
        $this->setOption("paginationSize", $pageSize);
952
        $this->setOption("paginationInitialPage", $initialPage);
953
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/6.2/page#counter
954
        return $this;
955
    }
956
957
    public function wizardProgressiveLoad(int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0, array $extraParams = []): self
958
    {
959
        $params = array_merge([
960
            'SecurityID' => SecurityToken::getSecurityID()
961
        ], $extraParams);
962
        $this->setProgressiveLoad($this->TempLink('load', false), $params, $pageSize, $initialPage, $mode, $scrollMargin);
963
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/6.2/sort#ajax-sort
964
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/6.2/filter#ajax-filter
965
        return $this;
966
    }
967
968
    /**
969
     * @link https://tabulator.info/docs/6.2/layout#responsive
970
     * @param boolean $startOpen
971
     * @param string $mode collapse|hide|flexCollapse
972
     * @return self
973
     */
974
    public function wizardResponsiveCollapse(bool $startOpen = false, string $mode = "collapse"): self
975
    {
976
        $this->setOption("responsiveLayout", $mode);
977
        $this->setOption("responsiveLayoutCollapseStartOpen", $startOpen);
978
        if ($mode != "hide") {
979
            $this->columns = array_merge([
980
                'ui_responsive_collapse' => [
981
                    "cssClass" => 'tabulator-cell-btn',
982
                    'formatter' => 'responsiveCollapse',
983
                    'headerSort' => false,
984
                    'width' => 40,
985
                ]
986
            ], $this->columns);
987
        }
988
        return $this;
989
    }
990
991
    public function wizardDataTree(bool $startExpanded = false, bool $filter = false, bool $sort = false, string $el = null): self
992
    {
993
        $this->setOption("dataTree", true);
994
        $this->setOption("dataTreeStartExpanded", $startExpanded);
995
        $this->setOption("dataTreeFilter", $filter);
996
        $this->setOption("dataTreeSort", $sort);
997
        if ($el) {
998
            $this->setOption("dataTreeElementColumn", $el);
999
        }
1000
        return $this;
1001
    }
1002
1003
    /**
1004
     * @param array $actions An array of bulk actions, that can extend the abstract one or use the generic with callbable
1005
     * @return self
1006
     */
1007
    public function wizardSelectable(array $actions = []): self
1008
    {
1009
        $this->columns = array_merge([
1010
            'ui_selectable' => [
1011
                "hozAlign" => 'center',
1012
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector',
1013
                'formatter' => 'rowSelection',
1014
                'titleFormatter' => 'rowSelection',
1015
                'width' => 40,
1016
                'maxWidth' => 40,
1017
                "headerSort" => false,
1018
            ]
1019
        ], $this->columns);
1020
        $this->setBulkActions($actions);
1021
        return $this;
1022
    }
1023
1024
    public function wizardMoveable(string $callback = "SSTabulator.rowMoved", $field = "Sort"): self
1025
    {
1026
        $this->moveUrl = $this->TempLink("item/{ID}/ajaxMove", false);
1027
        $this->setOption("movableRows", true);
1028
        $this->addListener("rowMoved", $callback);
1029
        $this->columns = array_merge([
1030
            'ui_move' => [
1031
                "hozAlign" => 'center',
1032
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector tabulator-ui-sort',
1033
                'rowHandle' => true,
1034
                'formatter' => 'handle',
1035
                'headerSort' => false,
1036
                'frozen' => true,
1037
                'width' => 40,
1038
                'maxWidth' => 40,
1039
            ],
1040
            // We need a hidden sort column
1041
            self::UI_SORT => [
1042
                "field" => $field,
1043
                'visible' => false,
1044
            ],
1045
        ], $this->columns);
1046
        return $this;
1047
    }
1048
1049
    /**
1050
     * @param string $field
1051
     * @param string $toggleElement arrow|header|false (header by default)
1052
     * @param boolean $isBool
1053
     * @return void
1054
     */
1055
    public function wizardGroupBy(string $field, string $toggleElement = 'header', bool $isBool = false)
1056
    {
1057
        $this->setOption("groupBy", $field);
1058
        $this->setOption("groupToggleElement", $toggleElement);
1059
        if ($isBool) {
1060
            $this->setOption("groupHeader", ['_fn' => self::JS_BOOL_GROUP_HEADER]);
1061
        }
1062
    }
1063
1064
    /**
1065
     * @param HTTPRequest $request
1066
     * @return HTTPResponse
1067
     */
1068
    public function handleItem($request)
1069
    {
1070
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1071
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1072
        $requestHandler = $this->getForm()->getController();
1073
        try {
1074
            $record = $this->getRecordFromRequest($request);
1075
        } catch (Exception $e) {
1076
            return $requestHandler->httpError(404, $e->getMessage());
1077
        }
1078
1079
        if (!$record) {
1080
            return $requestHandler->httpError(404, 'That record was not found');
1081
        }
1082
        $handler = $this->getItemRequestHandler($record, $requestHandler);
1083
        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...
1084
    }
1085
1086
    /**
1087
     * @param HTTPRequest $request
1088
     * @return HTTPResponse
1089
     */
1090
    public function handleTool($request)
1091
    {
1092
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1093
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1094
        $requestHandler = $this->getForm()->getController();
1095
        $tool = $this->getToolFromRequest($request);
1096
        if (!$tool) {
1097
            return $requestHandler->httpError(404, 'That tool was not found');
1098
        }
1099
        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...
1100
    }
1101
1102
    /**
1103
     * @param HTTPRequest $request
1104
     * @return HTTPResponse
1105
     */
1106
    public function handleBulkAction($request)
1107
    {
1108
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1109
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1110
        $requestHandler = $this->getForm()->getController();
1111
        $bulkAction = $this->getBulkActionFromRequest($request);
1112
        if (!$bulkAction) {
1113
            return $requestHandler->httpError(404, 'That bulk action was not found');
1114
        }
1115
        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...
1116
    }
1117
1118
    /**
1119
     * @return string name of {@see TabulatorGrid_ItemRequest} subclass
1120
     */
1121
    public function getItemRequestClass(): string
1122
    {
1123
        if ($this->itemRequestClass) {
1124
            return $this->itemRequestClass;
1125
        } elseif (ClassInfo::exists(static::class . '_ItemRequest')) {
1126
            return static::class . '_ItemRequest';
1127
        }
1128
        return TabulatorGrid_ItemRequest::class;
1129
    }
1130
1131
    /**
1132
     * Build a request handler for the given record
1133
     *
1134
     * @param DataObject $record
1135
     * @param RequestHandler $requestHandler
1136
     * @return TabulatorGrid_ItemRequest
1137
     */
1138
    protected function getItemRequestHandler($record, $requestHandler)
1139
    {
1140
        $class = $this->getItemRequestClass();
1141
        $assignedClass = $this->itemRequestClass;
1142
        $this->extend('updateItemRequestClass', $class, $record, $requestHandler, $assignedClass);
1143
        /** @var TabulatorGrid_ItemRequest $handler */
1144
        $handler = Injector::inst()->createWithArgs(
1145
            $class,
1146
            [$this, $record, $requestHandler]
1147
        );
1148
        if ($template = $this->getTemplate()) {
1149
            $handler->setTemplate($template);
1150
        }
1151
        $this->extend('updateItemRequestHandler', $handler);
1152
        return $handler;
1153
    }
1154
1155
    public function getStateKey(string $TabID = null)
1156
    {
1157
        $nested = [];
1158
        $form = $this->getForm();
1159
        $scope = $this->modelClass ? str_replace('_', '\\', $this->modelClass) :  "default";
1160
        if ($form) {
0 ignored issues
show
introduced by
$form is of type SilverStripe\Forms\Form, thus it always evaluated to true.
Loading history...
1161
            $controller = $form->getController();
1162
1163
            // We are in a nested form, track by id since each records needs it own state
1164
            while ($controller instanceof TabulatorGrid_ItemRequest) {
1165
                $record = $controller->getRecord();
1166
                $nested[str_replace('_', '\\', get_class($record))] = $record->ID;
1167
1168
                // Move to parent controller
1169
                $controller = $controller->getController();
1170
            }
1171
1172
            // Scope by top controller class
1173
            $scope = str_replace('_', '\\', get_class($controller));
1174
        }
1175
1176
        $baseKey = 'TabulatorState';
1177
        if ($TabID) {
1178
            $baseKey .= '_' . $TabID;
1179
        }
1180
        $name = $this->getName();
1181
        $key = "$baseKey.$scope.$name";
1182
        foreach ($nested as $k => $v) {
1183
            $key .= "$k.$v";
1184
        }
1185
        return $key;
1186
    }
1187
1188
    /**
1189
     * @param HTTPRequest|null $request
1190
     * @return array{'page': int, 'limit': int, 'sort': array, 'filter': array}
1191
     */
1192
    public function getState(HTTPRequest $request = null)
1193
    {
1194
        if ($request === null) {
1195
            $request = Controller::curr()->getRequest();
1196
        }
1197
        $TabID = $request->requestVar('TabID') ?? null;
1198
        $stateKey = $this->getStateKey($TabID);
1199
        $state = $request->getSession()->get($stateKey);
1200
        return $state ?? [
1201
            'page' => 1,
1202
            'limit' => $this->pageSize,
1203
            'sort' => [],
1204
            'filter' => [],
1205
        ];
1206
    }
1207
1208
    public function setState(HTTPRequest $request, $state)
1209
    {
1210
        $TabID = $request->requestVar('TabID') ?? null;
1211
        $stateKey = $this->getStateKey($TabID);
1212
        $request->getSession()->set($stateKey, $state);
1213
        // If we are in a new controller, we can clear other states
1214
        // Note: this would break tabbed navigation if you try to open multiple tabs, see below for more info
1215
        // @link https://github.com/silverstripe/silverstripe-framework/issues/9556
1216
        $matches = [];
1217
        preg_match_all('/\.(.*?)\./', $stateKey, $matches);
1218
        $scope = $matches[1][0] ?? null;
1219
        if ($scope) {
1220
            self::clearAllStates($scope);
1221
        }
1222
    }
1223
1224
    public function clearState(HTTPRequest $request)
1225
    {
1226
        $TabID = $request->requestVar('TabID') ?? null;
1227
        $stateKey = $this->getStateKey($TabID);
1228
        $request->getSession()->clear($stateKey);
1229
    }
1230
1231
    public static function clearAllStates(string $exceptScope = null, string $TabID = null)
1232
    {
1233
        $request = Controller::curr()->getRequest();
1234
        $baseKey = 'TabulatorState';
1235
        if ($TabID) {
1236
            $baseKey .= '_' . $TabID;
1237
        }
1238
        $allStates = $request->getSession()->get($baseKey);
1239
        if (!$allStates) {
1240
            return;
1241
        }
1242
        foreach ($allStates as $scope => $data) {
1243
            if ($exceptScope && $scope == $exceptScope) {
1244
                continue;
1245
            }
1246
            $request->getSession()->clear("TabulatorState.$scope");
1247
        }
1248
    }
1249
1250
    public function StateValue($key, $field): ?string
1251
    {
1252
        $state = $this->getState();
1253
        $arr = $state[$key] ?? [];
1254
        foreach ($arr as $s) {
1255
            if ($s['field'] === $field) {
1256
                return $s['value'];
1257
            }
1258
        }
1259
        return null;
1260
    }
1261
1262
    /**
1263
     * Provides autocomplete lists
1264
     *
1265
     * @param HTTPRequest $request
1266
     * @return HTTPResponse
1267
     */
1268
    public function autocomplete(HTTPRequest $request)
1269
    {
1270
        if ($this->isDisabled() || $this->isReadonly()) {
1271
            return $this->httpError(403);
1272
        }
1273
        $SecurityID = $request->getVar('SecurityID');
1274
        if (!SecurityToken::inst()->check($SecurityID)) {
1275
            return $this->httpError(404, "Invalid SecurityID");
1276
        }
1277
1278
        $name = $request->getVar("Column");
1279
        $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

1279
        $col = $this->getColumn(/** @scrutinizer ignore-type */ $name);
Loading history...
1280
        if (!$col) {
1281
            return $this->httpError(403, "Invalid column");
1282
        }
1283
1284
        // Don't use % term as it prevents use of indexes
1285
        $term = $request->getVar('term') . '%';
1286
        $term = str_replace(' ', '%', $term);
1287
1288
        $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

1288
        $parts = explode(".", /** @scrutinizer ignore-type */ $name);
Loading history...
1289
        if (count($parts) > 2) {
1290
            array_pop($parts);
1291
        }
1292
        if (count($parts) == 2) {
1293
            $class = $parts[0];
1294
            $field = $parts[1];
1295
        } elseif (count($parts) == 1) {
1296
            $class = preg_replace("/ID$/", "", $parts[0]);
1297
            $field = 'Title';
1298
        } else {
1299
            return $this->httpError(403, "Invalid field");
1300
        }
1301
1302
        /** @var DataObject $sng */
1303
        $sng = $class::singleton();
1304
        $baseTable = $sng->baseTable();
1305
1306
        $searchField = null;
1307
        $searchCandidates = [
1308
            $field, 'Name', 'Surname', 'Email', 'ID'
1309
        ];
1310
1311
        // Ensure field exists, this is really rudimentary
1312
        $db = $class::config()->db;
1313
        foreach ($searchCandidates as $searchCandidate) {
1314
            if ($searchField) {
1315
                continue;
1316
            }
1317
            if (isset($db[$searchCandidate])) {
1318
                $searchField = $searchCandidate;
1319
            }
1320
        }
1321
        $searchCols = [$searchField];
1322
1323
        // For members, do something better
1324
        if ($baseTable == 'Member') {
1325
            $searchField = ['FirstName', 'Surname'];
1326
            $searchCols = ['FirstName', 'Surname', 'Email'];
1327
        }
1328
1329
        if (!empty($col['editorParams']['customSearchField'])) {
1330
            $searchField = $col['editorParams']['customSearchField'];
1331
        }
1332
        if (!empty($col['editorParams']['customSearchCols'])) {
1333
            $searchCols = $col['editorParams']['customSearchCols'];
1334
        }
1335
1336
        // Note: we need to use the orm, even if it's slower, to make sure any extension is properly applied
1337
        /** @var DataList $list */
1338
        $list = $sng::get();
1339
1340
        // Make sure at least one field is not null...
1341
        $where = [];
1342
        foreach ($searchCols as $searchCol) {
1343
            $where[] = $searchCol . ' IS NOT NULL';
1344
        }
1345
        $list = $list->where($where);
1346
        // ... and matches search term ...
1347
        $where = [];
1348
        foreach ($searchCols as $searchCol) {
1349
            $where[$searchCol . ' LIKE ?'] = $term;
1350
        }
1351
        $list = $list->whereAny($where);
1352
1353
        // ... and any user set requirements
1354
        if (!empty($col['editorParams']['where'])) {
1355
            // Deal with in clause
1356
            $customWhere = [];
1357
            foreach ($col['editorParams']['where'] as $col => $param) {
1358
                // For array, we need a IN statement with a ? for each value
1359
                if (is_array($param)) {
1360
                    $prepValue = [];
1361
                    $params = [];
1362
                    foreach ($param as $paramValue) {
1363
                        $params[] = $paramValue;
1364
                        $prepValue[] = "?";
1365
                    }
1366
                    $customWhere["$col IN (" . implode(',', $prepValue) . ")"] = $params;
1367
                } else {
1368
                    $customWhere["$col = ?"] = $param;
1369
                }
1370
            }
1371
            $list = $list->where($customWhere);
1372
        }
1373
1374
        $results = iterator_to_array($list);
1375
        $data = [];
1376
        foreach ($results as $record) {
1377
            if (is_array($searchField)) {
1378
                $labelParts = [];
1379
                foreach ($searchField as $sf) {
1380
                    $labelParts[] = $record->$sf;
1381
                }
1382
                $label = implode(" ", $labelParts);
1383
            } else {
1384
                $label = $record->$searchField;
1385
            }
1386
            $data[] = [
1387
                'value' => $record->ID,
1388
                'label' => $label,
1389
            ];
1390
        }
1391
1392
        $json = json_encode($data);
1393
        $response = new HTTPResponse($json);
1394
        $response->addHeader('Content-Type', 'application/script');
1395
        return $response;
1396
    }
1397
1398
    /**
1399
     * @link http://www.tabulator.info/docs/6.2/page#remote-response
1400
     * @param HTTPRequest $request
1401
     * @return HTTPResponse
1402
     */
1403
    public function load(HTTPRequest $request)
1404
    {
1405
        if ($this->isDisabled() || $this->isReadonly()) {
1406
            return $this->httpError(403);
1407
        }
1408
        $SecurityID = $request->getVar('SecurityID');
1409
        if (!SecurityToken::inst()->check($SecurityID)) {
1410
            return $this->httpError(404, "Invalid SecurityID");
1411
        }
1412
1413
        $page = (int) $request->getVar('page');
1414
        $limit = (int) $request->getVar('size');
1415
1416
        $sort = $request->getVar('sort');
1417
        $filter = $request->getVar('filter');
1418
1419
        // Persist state to allow the ItemEditForm to display navigation
1420
        $state = [
1421
            'page' => $page,
1422
            'limit' => $limit,
1423
            'sort' => $sort,
1424
            'filter' => $filter,
1425
        ];
1426
        $this->setState($request, $state);
1427
1428
        $offset = ($page - 1) * $limit;
1429
        $data = $this->getManipulatedData($limit, $offset, $sort, $filter);
1430
        $data['state'] = $state;
1431
1432
        $encodedData = json_encode($data);
1433
        if (!$encodedData) {
1434
            throw new Exception(json_last_error_msg());
1435
        }
1436
1437
        $response = new HTTPResponse($encodedData);
1438
        $response->addHeader('Content-Type', 'application/json');
1439
        return $response;
1440
    }
1441
1442
    /**
1443
     * @param HTTPRequest $request
1444
     * @return DataObject|null
1445
     */
1446
    protected function getRecordFromRequest(HTTPRequest $request): ?DataObject
1447
    {
1448
        $id = $request->param('ID');
1449
        /** @var DataObject $record */
1450
        if (is_numeric($id)) {
1451
            /** @var Filterable $dataList */
1452
            $dataList = $this->getList();
1453
            $record = $dataList->byID($id);
1454
1455
            if (!$record) {
1456
                $record = DataObject::get_by_id($this->getModelClass(), $id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type boolean|integer expected by parameter $idOrCache of SilverStripe\ORM\DataObject::get_by_id(). ( Ignorable by Annotation )

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

1456
                $record = DataObject::get_by_id($this->getModelClass(), /** @scrutinizer ignore-type */ $id);
Loading history...
1457
                if ($record) {
1458
                    throw new RuntimeException('This record is not accessible from the list');
1459
                }
1460
            }
1461
        } else {
1462
            $record = Injector::inst()->create($this->getModelClass());
1463
        }
1464
        return $record;
1465
    }
1466
1467
    /**
1468
     * @param HTTPRequest $request
1469
     * @return AbstractTabulatorTool|null
1470
     */
1471
    protected function getToolFromRequest(HTTPRequest $request): ?AbstractTabulatorTool
1472
    {
1473
        $toolID = $request->param('ID');
1474
        $tool = $this->getTool($toolID);
1475
        return $tool;
1476
    }
1477
1478
    /**
1479
     * @param HTTPRequest $request
1480
     * @return AbstractBulkAction|null
1481
     */
1482
    protected function getBulkActionFromRequest(HTTPRequest $request): ?AbstractBulkAction
1483
    {
1484
        $toolID = $request->param('ID');
1485
        $tool = $this->getBulkAction($toolID);
1486
        return $tool;
1487
    }
1488
1489
    /**
1490
     * Get the value of a named field  on the given record.
1491
     *
1492
     * Use of this method ensures that any special rules around the data for this gridfield are
1493
     * followed.
1494
     *
1495
     * @param DataObject $record
1496
     * @param string $fieldName
1497
     *
1498
     * @return mixed
1499
     */
1500
    public function getDataFieldValue($record, $fieldName)
1501
    {
1502
        if ($record->hasMethod('relField')) {
1503
            return $record->relField($fieldName);
1504
        }
1505
1506
        if ($record->hasMethod($fieldName)) {
1507
            return $record->$fieldName();
1508
        }
1509
1510
        return $record->$fieldName;
1511
    }
1512
1513
    public function getManipulatedList(): SS_List
1514
    {
1515
        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...
1516
    }
1517
1518
    public function getList(): SS_List
1519
    {
1520
        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...
1521
    }
1522
1523
    public function setList(SS_List $list): self
1524
    {
1525
        if ($this->autoloadDataList && $list instanceof DataList) {
1526
            $this->wizardRemotePagination();
1527
        }
1528
        $this->list = $list;
1529
        return $this;
1530
    }
1531
1532
    public function hasArrayList(): bool
1533
    {
1534
        return $this->list instanceof ArrayList;
1535
    }
1536
1537
    public function getArrayList(): ArrayList
1538
    {
1539
        if (!$this->list instanceof ArrayList) {
1540
            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

1540
            throw new RuntimeException("Value is not a ArrayList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1541
        }
1542
        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...
1543
    }
1544
1545
    public function hasDataList(): bool
1546
    {
1547
        return $this->list instanceof DataList;
1548
    }
1549
1550
    /**
1551
     * A properly typed on which you can call byID
1552
     * @return ArrayList|DataList
1553
     */
1554
    public function getByIDList()
1555
    {
1556
        return $this->list;
1557
    }
1558
1559
    public function hasByIDList(): bool
1560
    {
1561
        return $this->hasDataList() || $this->hasArrayList();
1562
    }
1563
1564
    public function getDataList(): DataList
1565
    {
1566
        if (!$this->list instanceof DataList) {
1567
            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

1567
            throw new RuntimeException("Value is not a DataList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1568
        }
1569
        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...
1570
    }
1571
1572
    public function getManipulatedData(int $limit, int $offset, array $sort = null, array $filter = null): array
1573
    {
1574
        if (!$this->hasDataList()) {
1575
            $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

1575
            /** @scrutinizer ignore-call */ 
1576
            $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...
1576
1577
            $lastRow = $this->list->count();
1578
            $lastPage = ceil($lastRow / $limit);
1579
1580
            $result = [
1581
                'last_row' => $lastRow,
1582
                'last_page' => $lastPage,
1583
                'data' => $data,
1584
            ];
1585
1586
            return $result;
1587
        }
1588
1589
        $dataList = $this->getDataList();
1590
1591
        $schema = DataObject::getSchema();
1592
        $dataClass = $dataList->dataClass();
1593
1594
        /** @var DataObject $singleton */
1595
        $singleton = singleton($dataClass);
1596
        $opts = $this->getTabulatorOptions($singleton);
1597
        $resolutionMap = [];
1598
1599
        $sortSql = [];
1600
        if ($sort) {
1601
            foreach ($sort as $sortValues) {
1602
                $cols = array_keys($this->columns);
1603
                $field = $sortValues['field'];
1604
                $sortField = $field;
1605
                $sortClass = $dataClass;
1606
                if (!in_array($field, $cols)) {
1607
                    throw new Exception("Invalid sort field: $field");
1608
                }
1609
                $dir = $sortValues['dir'];
1610
                if (!in_array($dir, ['asc', 'desc'])) {
1611
                    throw new Exception("Invalid sort dir: $dir");
1612
                }
1613
1614
                // Nested sort
1615
                if (str_contains($field, '.')) {
1616
                    $parts = explode(".", $field);
1617
                    $relationName = $parts[0];
1618
1619
                    // Resolve relation only once in case of multiples similar keys
1620
                    if (!isset($resolutionMap[$relationName])) {
1621
                        $resolutionMap[$relationName] = $singleton->relObject($relationName);
1622
                    }
1623
                    // Not matching anything (maybe a formatting .Nice ?)
1624
                    $resolvedObject = $resolutionMap[$relationName] ?? null;
1625
                    if (!$resolvedObject) {
1626
                        continue;
1627
                    }
1628
                    // Maybe it's an helper method like .Nice and it's not sortable in the query
1629
                    if (!($resolvedObject instanceof DataList) && !($resolvedObject instanceof DataObject)) {
1630
                        $field = $parts[0];
0 ignored issues
show
Unused Code introduced by
The assignment to $field is dead and can be removed.
Loading history...
1631
                        continue;
1632
                    }
1633
                    $sortClass = get_class($resolvedObject);
1634
                    $sortField = $parts[1];
1635
                    $tableName = $schema->tableForField($sortClass, $sortField);
1636
                    $baseIDColumn = $schema->sqlColumnForField($dataClass, 'ID');
1637
                    $dataList = $dataList->leftJoin($tableName, "\"{$relationName}\".\"ID\" = {$baseIDColumn}", $relationName);
1638
                }
1639
1640
                // Is it an actual field or an expression ?
1641
                $sortedField = $schema->tableForField($sortClass, $sortField);
1642
                if ($sortedField) {
1643
                    $sortSql[] = $field . ' ' . $dir;
1644
                }
1645
            }
1646
        } else {
1647
            // If we have a sort column
1648
            if (isset($this->columns[self::UI_SORT])) {
1649
                $sortSql[] = $this->columns[self::UI_SORT]['field'] . ' ASC';
1650
            }
1651
        }
1652
        if (!empty($sortSql)) {
1653
            $dataList = $dataList->sort(implode(", ", $sortSql));
1654
        }
1655
1656
        // Filtering is an array of field/type/value arrays
1657
        $filters = [];
1658
        $anyFilters = [];
1659
        $where = [];
1660
        $anyWhere = [];
1661
        if ($filter) {
1662
            $searchAliases = $opts['searchAliases'] ?? [];
1663
            $searchAliases = array_flip($searchAliases);
1664
            foreach ($filter as $filterValues) {
1665
                $cols = array_keys($this->columns);
1666
                $field = $filterValues['field'];
1667
                if (strpos($field, '__') !== 0 && !in_array($field, $cols)) {
1668
                    throw new Exception("Invalid filter field: $field");
1669
                }
1670
                // If .Nice was used
1671
                $field = str_replace('.Nice', '', $field);
1672
1673
                $field = $searchAliases[$field] ?? $field;
1674
                $value = $filterValues['value'];
1675
                $type = $filterValues['type'];
1676
1677
                // Some types of fields need custom sql expressions (eg uuids)
1678
                $fieldInstance = $singleton->dbObject($field);
1679
                if ($fieldInstance->hasMethod('filterExpression')) {
1680
                    $where[] = $fieldInstance->filterExpression($type, $value);
1681
                    continue;
1682
                }
1683
1684
                $rawValue = $value;
1685
1686
                // Strict value
1687
                if ($value === "true") {
1688
                    $value = true;
1689
                } elseif ($value === "false") {
1690
                    $value = false;
1691
                }
1692
1693
                switch ($type) {
1694
                    case "=":
1695
                        if ($field === "__wildcard") {
1696
                            // It's a wildcard search
1697
                            $anyFilters = $this->createWildcardFilters($rawValue);
1698
                        } elseif ($field === "__quickfilter") {
1699
                            // It's a quickfilter search
1700
                            $this->createQuickFilter($rawValue, $dataList);
1701
                        } else {
1702
                            $filters["$field"] = $value;
1703
                        }
1704
                        break;
1705
                    case "!=":
1706
                        $filters["$field:not"] = $value;
1707
                        break;
1708
                    case "like":
1709
                        $filters["$field:PartialMatch:nocase"] = $value;
1710
                        break;
1711
                    case "keywords":
1712
                        $filters["$field:PartialMatch:nocase"] = str_replace(" ", "%", $value);
1713
                        break;
1714
                    case "starts":
1715
                        $filters["$field:StartsWith:nocase"] = $value;
1716
                        break;
1717
                    case "ends":
1718
                        $filters["$field:EndsWith:nocase"] = $value;
1719
                        break;
1720
                    case "<":
1721
                        $filters["$field:LessThan:nocase"] = $value;
1722
                        break;
1723
                    case "<=":
1724
                        $filters["$field:LessThanOrEqual:nocase"] = $value;
1725
                        break;
1726
                    case ">":
1727
                        $filters["$field:GreaterThan:nocase"] = $value;
1728
                        break;
1729
                    case ">=":
1730
                        $filters["$field:GreaterThanOrEqual:nocase"] = $value;
1731
                        break;
1732
                    case "in":
1733
                        $filters["$field"] = $value;
1734
                        break;
1735
                    case "regex":
1736
                        $dataList = $dataList->filters('REGEXP ' . Convert::raw2sql($value));
0 ignored issues
show
Bug introduced by
Are you sure SilverStripe\Core\Convert::raw2sql($value) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

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