Passed
Push — master ( 47e01d...760836 )
by Thomas
12:05
created

TabulatorGrid::getArrayList()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 6
rs 10
cc 2
nc 2
nop 0
1
<?php
2
3
namespace LeKoala\Tabulator;
4
5
use Exception;
6
use RuntimeException;
7
use SilverStripe\i18n\i18n;
8
use SilverStripe\Forms\Form;
9
use InvalidArgumentException;
10
use SilverStripe\ORM\SS_List;
11
use SilverStripe\Core\Convert;
12
use SilverStripe\ORM\DataList;
13
use SilverStripe\ORM\ArrayList;
14
use SilverStripe\Core\ClassInfo;
15
use SilverStripe\ORM\DataObject;
16
use SilverStripe\Forms\FieldList;
17
use SilverStripe\Forms\FormField;
18
use SilverStripe\Control\Director;
19
use SilverStripe\View\Requirements;
20
use SilverStripe\Control\Controller;
21
use SilverStripe\Control\HTTPRequest;
22
use SilverStripe\Control\HTTPResponse;
23
use SilverStripe\Control\RequestHandler;
24
use SilverStripe\Core\Injector\Injector;
25
use SilverStripe\Security\SecurityToken;
26
use SilverStripe\ORM\FieldType\DBBoolean;
27
use SilverStripe\Forms\GridField\GridFieldConfig;
28
use SilverStripe\Core\Manifest\ModuleResourceLoader;
29
use SilverStripe\ORM\FieldType\DBEnum;
30
31
/**
32
 * This is a replacement for most GridField usages in SilverStripe
33
 * It can easily work in the frontend too
34
 *
35
 * @link http://www.tabulator.info/
36
 */
37
class TabulatorGrid extends FormField
38
{
39
    const POS_START = 'start';
40
    const POS_END = 'end';
41
42
    // @link http://www.tabulator.info/examples/5.2?#fittodata
43
    const LAYOUT_FIT_DATA = "fitData";
44
    const LAYOUT_FIT_DATA_FILL = "fitDataFill";
45
    const LAYOUT_FIT_DATA_STRETCH = "fitDataStretch";
46
    const LAYOUT_FIT_DATA_TABLE = "fitDataTable";
47
    const LAYOUT_FIT_COLUMNS = "fitColumns";
48
49
    const RESPONSIVE_LAYOUT_HIDE = "hide";
50
    const RESPONSIVE_LAYOUT_COLLAPSE = "collapse";
51
52
    // @link http://www.tabulator.info/docs/5.2/format
53
    const FORMATTER_PLAINTEXT = 'plaintext';
54
    const FORMATTER_TEXTAREA = 'textarea';
55
    const FORMATTER_HTML = 'html';
56
    const FORMATTER_MONEY = 'money';
57
    const FORMATTER_IMAGE = 'image';
58
    const FORMATTER_LINK = 'link';
59
    const FORMATTER_DATETIME = 'datetime';
60
    const FORMATTER_DATETIME_DIFF = 'datetimediff';
61
    const FORMATTER_TICKCROSS = 'tickCross';
62
    const FORMATTER_COLOR = 'color';
63
    const FORMATTER_STAR = 'star';
64
    const FORMATTER_TRAFFIC = 'traffic';
65
    const FORMATTER_PROGRESS = 'progress';
66
    const FORMATTER_LOOKUP = 'lookup';
67
    const FORMATTER_BUTTON_TICK = 'buttonTick';
68
    const FORMATTER_BUTTON_CROSS = 'buttonCross';
69
    const FORMATTER_ROWNUM = 'rownum';
70
    const FORMATTER_HANDLE = 'handle';
71
    // @link http://www.tabulator.info/docs/5.2/format#format-module
72
    const FORMATTER_ROW_SELECTION = 'rowSelection';
73
    const FORMATTER_RESPONSIVE_COLLAPSE = 'responsiveCollapse';
74
75
    // our built in functions
76
    const JS_FLAG_FORMATTER = 'SSTabulator.flagFormatter';
77
    const JS_BUTTON_FORMATTER = 'SSTabulator.buttonFormatter';
78
    const JS_CUSTOM_TICK_CROSS_FORMATTER = 'SSTabulator.customTickCrossFormatter';
79
    const JS_BOOL_GROUP_HEADER = 'SSTabulator.boolGroupHeader';
80
    const JS_SIMPLE_ROW_FORMATTER = 'SSTabulator.simpleRowFormatter';
81
    const JS_EXPAND_TOOLTIP = 'SSTabulator.expandTooltip';
82
    const JS_DATA_AJAX_RESPONSE = 'SSTabulator.dataAjaxResponse';
83
84
    /**
85
     * @config
86
     */
87
    private static array $allowed_actions = [
88
        'load',
89
        'handleItem',
90
        'handleTool',
91
        'configProvider',
92
        'autocomplete',
93
        'handleBulkAction',
94
    ];
95
96
    private static $url_handlers = [
97
        'item/$ID' => 'handleItem',
98
        'tool/$ID' => 'handleTool',
99
        'bulkAction/$ID' => 'handleBulkAction',
100
    ];
101
102
    private static array $casting = [
103
        'JsonOptions' => 'HTMLFragment',
104
        'ShowTools' => 'HTMLFragment',
105
        'dataAttributesHTML' => 'HTMLFragment',
106
    ];
107
108
    /**
109
     * @config
110
     */
111
    private static string $theme = 'bootstrap5';
112
113
    /**
114
     * @config
115
     */
116
    private static string $version = '5.2.7';
117
118
    /**
119
     * @config
120
     */
121
    private static string $luxon_version = '2.3.1';
122
123
    /**
124
     * @config
125
     */
126
    private static string $last_icon_version = '1.3.3';
127
128
    /**
129
     * @config
130
     */
131
    private static bool $use_cdn = false;
132
133
    /**
134
     * @config
135
     */
136
    private static bool $use_custom_build = true;
137
138
    /**
139
     * @config
140
     */
141
    private static bool $enable_luxon = false;
142
143
    /**
144
     * @config
145
     */
146
    private static bool $enable_last_icon = false;
147
148
    /**
149
     * @config
150
     */
151
    private static bool $enable_requirements = true;
152
153
    /**
154
     * @link http://www.tabulator.info/docs/5.2/options
155
     * @config
156
     */
157
    private static array $default_options = [
158
        'index' => "ID", // http://tabulator.info/docs/5.2/data#row-index
159
        'layout' => 'fitColumns', // http://www.tabulator.info/docs/5.2/layout#layout
160
        'height' => '100%', // http://www.tabulator.info/docs/5.2/layout#height-fixed
161
        // 'maxHeight' => "100%",
162
        'responsiveLayout' => "hide", // http://www.tabulator.info/docs/5.2/layout#responsive
163
        'rowFormatter' => "SSTabulator.simpleRowFormatter", // http://tabulator.info/docs/5.2/format#row
164
    ];
165
166
    /**
167
     * @link http://tabulator.info/docs/5.2/columns#defaults
168
     * @config
169
     */
170
    private static array $default_column_options = [
171
        'resizable' => false,
172
        'tooltip' => 'SSTabulator.expandTooltip',
173
    ];
174
175
    private static bool $enable_ajax_init = true;
176
177
    /**
178
     * @config
179
     */
180
    private static array $custom_pagination_icons = [];
181
182
    /**
183
     * Data source.
184
     */
185
    protected ?SS_List $list;
186
187
    /**
188
     * @link http://www.tabulator.info/docs/5.2/columns
189
     */
190
    protected array $columns = [];
191
192
    /**
193
     * @link http://tabulator.info/docs/5.2/columns#defaults
194
     */
195
    protected array $columnDefaults = [];
196
197
    /**
198
     * @link http://www.tabulator.info/docs/5.2/options
199
     */
200
    protected array $options = [];
201
202
    protected bool $autoloadDataList = true;
203
204
    protected bool $rowClickTriggersAction = false;
205
206
    protected int $pageSize = 10;
207
208
    protected string $itemRequestClass = '';
209
210
    protected string $modelClass = '';
211
212
    protected bool $lazyInit = false;
213
214
    protected array $tools = [];
215
216
    /**
217
     * @var AbstractBulkAction[]
218
     */
219
    protected array $bulkActions = [];
220
221
    protected array $listeners = [];
222
223
    protected array $jsNamespaces = [
224
        'SSTabulator'
225
    ];
226
227
    protected array $linksOptions = [
228
        'ajaxURL'
229
    ];
230
231
    protected array $dataAttributes = [];
232
233
    protected string $controllerFunction = "";
234
235
    protected bool $useConfigProvider = true;
236
237
    protected string $editUrl = "";
238
239
    protected string $moveUrl = "";
240
241
    protected string $bulkUrl = "";
242
243
    /**
244
     * @param string $fieldName
245
     * @param string|null $title
246
     * @param SS_List $value
247
     */
248
    public function __construct($name, $title = null, $value = null)
249
    {
250
        parent::__construct($name, $title, $value);
251
        $this->options = self::config()->default_options ?? [];
252
        $this->columnDefaults = self::config()->default_column_options ?? [];
253
254
        // We don't want regular setValue for this since it would break with loadFrom logic
255
        if ($value) {
256
            $this->setList($value);
257
        }
258
    }
259
260
    /**
261
     * This helps if some third party code expects the TabulatorGrid to be a GridField
262
     * Only works to a really basic extent
263
     */
264
    public function getConfig(): GridFieldConfig
265
    {
266
        return new GridFieldConfig;
267
    }
268
269
    /**
270
     * Temporary link that will be replaced by a real link by processLinks
271
     *
272
     * @param string $action
273
     * @return string
274
     */
275
    public function TempLink(string $action, bool $controller = true): string
276
    {
277
        // It's an absolute link
278
        if (strpos($action, '/') === 0 || strpos($action, 'http') === 0) {
279
            return $action;
280
        }
281
        // Already temp
282
        if (strpos($action, ':') !== false) {
283
            return $action;
284
        }
285
        $prefix = $controller ? "controller" : "form";
286
        return "$prefix:$action";
287
    }
288
289
    public function ControllerLink(string $action): string
290
    {
291
        return $this->getForm()->getController()->Link($action);
292
    }
293
294
    public function getCreateLink(): string
295
    {
296
        return Controller::join_links($this->Link('item'), 'new');
297
    }
298
299
    /**
300
     * @param FieldList $fields
301
     * @param string $name
302
     * @return TabulatorGrid|null
303
     */
304
    public static function replaceGridField(FieldList $fields, string $name)
305
    {
306
        /** @var \SilverStripe\Forms\GridField\GridField $gridField */
307
        $gridField = $fields->dataFieldByName($name);
308
        if (!$gridField) {
0 ignored issues
show
introduced by
$gridField is of type SilverStripe\Forms\GridField\GridField, thus it always evaluated to true.
Loading history...
309
            return;
310
        }
311
        $tabulatorGrid = new TabulatorGrid($name, $gridField->Title(), $gridField->getList());
312
        // In the cms, this is mostly never happening
313
        if ($gridField->getForm()) {
314
            $tabulatorGrid->setForm($gridField->getForm());
315
        }
316
        $tabulatorGrid->configureFromDataObject($gridField->getModelClass());
317
        $tabulatorGrid->setLazyInit(true);
318
        $fields->replaceField($name, $tabulatorGrid);
319
320
        return $tabulatorGrid;
321
    }
322
323
    public function configureFromDataObject($className = null, bool $clear = true): void
0 ignored issues
show
Unused Code introduced by
The parameter $clear is not used and could be removed. ( Ignorable by Annotation )

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

323
    public function configureFromDataObject($className = null, /** @scrutinizer ignore-unused */ bool $clear = true): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
324
    {
325
        $this->columns = [];
326
327
        if (!$className) {
328
            $className = $this->getModelClass();
329
        }
330
        if (!$className) {
331
            throw new RuntimeException("Could not find the model class");
332
        }
333
        $this->modelClass = $className;
334
335
        /** @var DataObject $singl */
336
        $singl = singleton($className);
337
338
        // Mock some base columns using SilverStripe built-in methods
339
        $columns = [];
340
        foreach ($singl->summaryFields() as $field => $title) {
341
            $title = str_replace(".", " ", $title);
342
            $columns[$field] = [
343
                'field' => $field,
344
                'title' => $title,
345
            ];
346
347
            $dbObject = $singl->dbObject($field);
348
            if ($dbObject) {
349
                if ($dbObject instanceof DBBoolean) {
350
                    $columns[$field]['formatter'] = "SSTabulator.customTickCrossFormatter";
351
                }
352
            }
353
        }
354
        foreach ($singl->searchableFields() as $key => $searchOptions) {
355
            /*
356
            "filter" => "NameOfTheFilter"
357
            "field" => "SilverStripe\Forms\FormField"
358
            "title" => "Title of the field"
359
            */
360
            if (!isset($columns[$key])) {
361
                continue;
362
            }
363
            $columns[$key]['headerFilter'] = true;
364
            // $columns[$key]['headerFilterPlaceholder'] = $searchOptions['title'];
365
            //TODO: implement filter mapping
366
            switch ($searchOptions['filter']) {
367
                default:
368
                    $columns[$key]['headerFilterFunc'] =  "like";
369
                    break;
370
            }
371
372
            // Restrict based on data type
373
            $dbObject = $singl->dbObject($key);
374
            if ($dbObject) {
375
                if ($dbObject instanceof DBBoolean) {
376
                    $columns[$key]['headerFilter'] = 'tickCross';
377
                    $columns[$key]['headerFilterFunc'] =  "=";
378
                    $columns[$key]['headerFilterParams'] =  [
379
                        'tristate' => true
380
                    ];
381
                }
382
                if ($dbObject instanceof DBEnum) {
383
                    $columns[$key]['headerFilter'] = 'list';
384
                    $columns[$key]['headerFilterFunc'] =  "=";
385
                    $columns[$key]['headerFilterParams'] =  [
386
                        'values' => $dbObject->enumValues()
387
                    ];
388
                }
389
            }
390
        }
391
392
        // Allow customizing our columns based on record
393
        if ($singl->hasMethod('tabulatorColumns')) {
394
            $fields = $singl->tabulatorColumns();
395
            if (!is_array($fields)) {
396
                throw new RuntimeException("tabulatorColumns must return an array");
397
            }
398
            foreach ($fields as $key => $columnOptions) {
399
                $baseOptions = $columns[$key] ?? [];
400
                $columns[$key] = array_merge($baseOptions, $columnOptions);
401
            }
402
        }
403
404
        $this->extend('updateConfiguredColumns', $columns);
405
406
        foreach ($columns as $col) {
407
            $this->addColumn($col['field'], $col['title'], $col);
408
        }
409
410
        // Sortable ?
411
        if ($singl->hasField('Sort')) {
412
            $this->wizardMoveable();
413
        }
414
415
        // Actions
416
        // We use a pseudo link, because maybe we cannot call Link() yet if it's not linked to a form
417
418
        $this->bulkUrl = $this->TempLink("bulkAction/", false);
419
420
        // - Core actions, handled by TabulatorGrid
421
        $itemUrl = $this->TempLink('item/{ID}', false);
422
        if ($singl->canEdit()) {
423
            $this->addButton("ui_edit", $itemUrl, "edit", "Edit");
424
            $this->editUrl = $this->TempLink("item/{ID}/ajaxEdit", false);
425
        } elseif ($singl->canView()) {
426
            $this->addButton("ui_view", $itemUrl, "visibility", "View");
427
        }
428
429
        // - Tools
430
        $this->tools = [];
431
        if ($singl->canCreate()) {
432
            $this->addTool(self::POS_START, new TabulatorAddNewButton($this));
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

432
            $this->addTool(self::POS_START, /** @scrutinizer ignore-call */ new TabulatorAddNewButton($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...
433
        }
434
435
        // - Custom actions are forwarded to the model itself
436
        if ($singl->hasMethod('tabulatorRowActions')) {
437
            $rowActions = $singl->tabulatorRowActions();
438
            if (!is_array($rowActions)) {
439
                throw new RuntimeException("tabulatorRowActions must return an array");
440
            }
441
            foreach ($rowActions as $key => $actionConfig) {
442
                $action = $actionConfig['action'] ?? $key;
443
                $url = $this->TempLink("item/{ID}/customAction/$action", false);
444
                $icon = $actionConfig['icon'] ?? "cog";
445
                $title = $actionConfig['title'] ?? "";
446
447
                $button = $this->makeButton($url, $icon, $title);
448
                if (!empty($actionConfig['ajax'])) {
449
                    $button['formatterParams']['ajax'] = true;
450
                }
451
                $this->addButtonFromArray("ui_customaction_$action", $button);
452
            }
453
        }
454
455
        $this->setRowClickTriggersAction(true);
456
    }
457
458
    public static function requirements(): void
459
    {
460
        $use_cdn = self::config()->use_cdn;
461
        $use_custom_build = self::config()->use_custom_build;
462
        $theme = self::config()->theme; // simple, midnight, modern or framework
463
        $version = self::config()->version;
464
        $luxon_version = self::config()->luxon_version;
465
        $enable_luxon = self::config()->enable_luxon;
466
        $last_icon_version = self::config()->last_icon_version;
467
        $enable_last_icon = self::config()->enable_last_icon;
468
469
        if ($use_cdn) {
470
            $baseDir = "https://cdn.jsdelivr.net/npm/tabulator-tables@$version/dist";
471
        } else {
472
            $asset = ModuleResourceLoader::resourceURL('lekoala/silverstripe-tabulator:client/cdn/js/tabulator.min.js');
473
            $baseDir = dirname(dirname($asset));
474
        }
475
476
        if ($luxon_version && $enable_luxon) {
477
            Requirements::javascript("https://cdn.jsdelivr.net/npm/luxon@$luxon_version/build/global/luxon.min.js");
478
        }
479
        if ($last_icon_version && $enable_last_icon) {
480
            Requirements::css("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.css");
481
            Requirements::javascript("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.js");
482
        }
483
        if ($use_custom_build) {
484
            // if (Director::isDev() && !Director::is_ajax()) {
485
            //     Requirements::javascript("lekoala/silverstripe-tabulator:client/custom-tabulator.js");
486
            // } else {
487
            Requirements::javascript("lekoala/silverstripe-tabulator:client/custom-tabulator.min.js");
488
            // }
489
            Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js');
490
        } else {
491
            Requirements::javascript("$baseDir/js/tabulator.min.js");
492
            Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js');
493
        }
494
495
        if ($theme) {
496
            Requirements::css("$baseDir/css/tabulator_$theme.min.css");
497
        } else {
498
            Requirements::css("$baseDir/css/tabulator.min.css");
499
        }
500
501
        if ($theme && $theme == "bootstrap5") {
502
            Requirements::css('lekoala/silverstripe-tabulator:client/custom-tabulator.min.css');
503
        }
504
    }
505
506
    public function setValue($value, $data = null)
507
    {
508
        if ($value instanceof DataList) {
509
            $this->configureFromDataObject($value->dataClass());
510
        }
511
        return parent::setValue($value, $data);
512
    }
513
514
    public function Field($properties = [])
515
    {
516
        $this->addExtraClass(self::config()->theme);
517
        if ($this->lazyInit) {
518
            $this->addExtraClass("lazy-loadable");
519
        }
520
        if (self::config()->enable_requirements) {
521
            self::requirements();
522
        }
523
524
        // Make sure we can use a standalone version of the field without a form
525
        // Function should match the name
526
        if (!$this->form) {
527
            $this->form = new Form(Controller::curr(), $this->getControllerFunction());
528
        }
529
530
        // Data attributes for our custom behaviour
531
        $this->setDataAttribute("row-click-triggers-action", $this->rowClickTriggersAction);
532
        $customIcons = self::config()->custom_pagination_icons;
533
        $this->setDataAttribute("use-custom-pagination-icons", empty($customIcons));
534
535
        $this->setDataAttribute("listeners", $this->listeners);
536
        if ($this->editUrl) {
537
            $url = $this->processLink($this->editUrl);
538
            $this->setDataAttribute("edit-url", $url);
539
        }
540
        if ($this->moveUrl) {
541
            $url = $this->processLink($this->moveUrl);
542
            $this->setDataAttribute("move-url", $url);
543
        }
544
        if (!empty($this->bulkActions)) {
545
            $url = $this->processLink($this->bulkUrl);
546
            $this->setDataAttribute("bulk-url", $url);
547
        }
548
549
        if ($this->useConfigProvider) {
550
            $configLink = "/" . ltrim($this->Link("configProvider"), "/");
551
            $configLink .= "?t=" . time();
552
            // This cannot be loaded as a js module
553
            Requirements::javascript($configLink, ['type' => 'application/javascript', 'defer' => 'true']);
554
        } else {
555
            Requirements::customScript($this->getInitScript());
556
        }
557
558
        return parent::Field($properties);
559
    }
560
561
    public function ShowTools(): string
562
    {
563
        if (empty($this->tools)) {
564
            return '';
565
        }
566
        $html = '';
567
        $html .= '<div class="tabulator-tools">';
568
        $html .= '<div class="tabulator-tools-start">';
569
        foreach ($this->tools as $tool) {
570
            if ($tool['position'] != self::POS_START) {
571
                continue;
572
            }
573
            $html .= ($tool['tool'])->forTemplate();
574
        }
575
        $html .= '</div>';
576
        $html .= '<div class="tabulator-tools-end">';
577
        foreach ($this->tools as $tool) {
578
            if ($tool['position'] != self::POS_END) {
579
                continue;
580
            }
581
            $html .= ($tool['tool'])->forTemplate();
582
        }
583
        // Show bulk actions at the end
584
        if (!empty($this->bulkActions)) {
585
            $selectLabel = _t(__CLASS__ . ".BULKSELECT", "Select a bulk action");
586
            $confirmLabel = _t(__CLASS__ . ".BULKCONFIRM", "Go");
587
            $html .= "<select class=\"tabulator-bulk-select\">";
588
            $html .= "<option>" . $selectLabel . "</option>";
589
            foreach ($this->bulkActions as $bulkAction) {
590
                $v = $bulkAction->getName();
591
                $xhr = $bulkAction->getXhr();
592
                $destructive = $bulkAction->getDestructive();
593
                $html .= "<option value=\"$v\" data-xhr=\"$xhr\" data-destructive=\"$destructive\">" . $bulkAction->getLabel() . "</option>";
594
            }
595
            $html .= "</select>";
596
            $html .= "<button class=\"tabulator-bulk-confirm btn\">" . $confirmLabel . "</button>";
597
        }
598
        $html .= '</div>';
599
        $html .= '</div>';
600
        return $html;
601
    }
602
603
    public function JsonOptions(): string
604
    {
605
        $this->processLinks();
606
607
        $data = $this->list ?? [];
608
        if ($this->autoloadDataList && $data instanceof DataList) {
609
            $data = null;
610
        }
611
        $opts = $this->options;
612
        $opts['columnDefaults'] = $this->columnDefaults;
613
614
        if (empty($this->columns)) {
615
            $opts['autoColumns'] = true;
616
        } else {
617
            $opts['columns'] = array_values($this->columns);
618
        }
619
620
        if ($data && is_iterable($data)) {
621
            if ($data instanceof ArrayList) {
622
                $data = $data->toArray();
623
            } else {
624
                if (is_iterable($data) && !is_array($data)) {
625
                    $data = iterator_to_array($data);
626
                }
627
            }
628
            $opts['data'] = $data;
629
        }
630
631
        // i18n
632
        $locale = strtolower(str_replace('_', '-', i18n::get_locale()));
633
        $paginationTranslations = [
634
            "first" => _t("TabulatorPagination.first", "First"),
635
            "first_title" =>  _t("TabulatorPagination.first_title", "First Page"),
636
            "last" =>  _t("TabulatorPagination.last", "Last"),
637
            "last_title" => _t("TabulatorPagination.last_title", "Last Page"),
638
            "prev" => _t("TabulatorPagination.prev", "Previous"),
639
            "prev_title" =>  _t("TabulatorPagination.prev_title", "Previous Page"),
640
            "next" => _t("TabulatorPagination.next", "Next"),
641
            "next_title" =>  _t("TabulatorPagination.next_title", "Next Page"),
642
            "all" =>  _t("TabulatorPagination.all", "All"),
643
        ];
644
        // This will always default to last icon if present
645
        $customIcons = self::config()->custom_pagination_icons;
646
        if (!empty($customIcons)) {
647
            $paginationTranslations['first'] = $customIcons['first'] ?? "<<";
648
            $paginationTranslations['last'] = $customIcons['last'] ?? ">>";
649
            $paginationTranslations['prev'] = $customIcons['prev'] ?? "<";
650
            $paginationTranslations['next'] = $customIcons['next'] ?? ">";
651
        }
652
        $dataTranslations = [
653
            "loading" => _t("TabulatorData.loading", "Loading"),
654
            "error" => _t("TabulatorData.error", "Error"),
655
        ];
656
        $groupsTranslations = [
657
            "item" => _t("TabulatorGroups.item", "Item"),
658
            "items" => _t("TabulatorGroups.items", "Items"),
659
        ];
660
        $headerFiltersTranslations = [
661
            "default" => _t("TabulatorHeaderFilters.default", "filter column..."),
662
        ];
663
        $bulkActionsTranslations = [
664
            "no_action" => _t("TabulatorBulkActions.no_action", "Please select an action"),
665
            "no_records" => _t("TabulatorBulkActions.no_records", "Please select a record"),
666
            "destructive" => _t("TabulatorBulkActions.destructive", "Confirm destructive action ?"),
667
        ];
668
        $translations = [
669
            'data' => $dataTranslations,
670
            'groups' => $groupsTranslations,
671
            'pagination' => $paginationTranslations,
672
            'headerFilters' => $headerFiltersTranslations,
673
            'bulkActions' => $bulkActionsTranslations,
674
        ];
675
        $opts['locale'] = $locale;
676
        $opts['langs'] = [
677
            $locale => $translations
678
        ];
679
680
        $format = Director::isDev() ? JSON_PRETTY_PRINT : 0;
681
        $json = json_encode($opts, $format);
682
683
        // Escape functions by namespace (see TabulatorField.js)
684
        foreach ($this->jsNamespaces as $ns) {
685
            $json = preg_replace('/"(' . $ns . '\.[a-zA-Z]*)"/', "$1", $json);
686
            // Keep static namespaces
687
            $json = str_replace("*" . $ns, $ns, $json);
688
        }
689
690
        return $json;
691
    }
692
693
    /**
694
     * @param Controller $controller
695
     * @return CompatLayerInterface
696
     */
697
    public function getCompatLayer(Controller $controller)
698
    {
699
        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...
700
            return new SilverstripeAdminCompat();
701
        }
702
        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...
703
            return new AdminiCompat();
704
        }
705
    }
706
707
    public function getAttributes()
708
    {
709
        $attrs = parent::getAttributes();
710
        unset($attrs['type']);
711
        unset($attrs['name']);
712
        return $attrs;
713
    }
714
715
    public function getOption(string $k)
716
    {
717
        return $this->options[$k] ?? null;
718
    }
719
720
    public function setOption(string $k, $v): self
721
    {
722
        $this->options[$k] = $v;
723
        return $this;
724
    }
725
726
    public function makeHeadersSticky(): self
727
    {
728
        $this->addExtraClass("tabulator-sticky");
729
        return $this;
730
    }
731
732
    public function setRemoteSource(string $url, array $extraParams = [], bool $dataResponse = false): self
733
    {
734
        $this->setOption("ajaxURL", $url); //set url for ajax request
735
        $params = array_merge([
736
            'SecurityID' => SecurityToken::getSecurityID()
737
        ], $extraParams);
738
        $this->setOption("ajaxParams", $params);
739
        // Accept response where data is nested under the data key
740
        if ($dataResponse) {
741
            $this->setOption("ajaxResponse", self::JS_DATA_AJAX_RESPONSE);
742
        }
743
        return $this;
744
    }
745
746
    /**
747
     * @link http://www.tabulator.info/docs/5.2/page#remote
748
     * @param string $url
749
     * @param array $params
750
     * @param integer $pageSize
751
     * @param integer $initialPage
752
     */
753
    public function setRemotePagination(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1): self
754
    {
755
        $this->setOption("pagination", true); //enable pagination
756
        $this->setOption("paginationMode", 'remote'); //enable remote pagination
757
        $this->setRemoteSource($url, $params);
758
        if (!$pageSize) {
759
            $pageSize = $this->pageSize;
760
        }
761
        $this->setOption("paginationSize", $pageSize);
762
        $this->setOption("paginationInitialPage", $initialPage);
763
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/5.2/page#counter
764
        return $this;
765
    }
766
767
    public function wizardRemotePagination(int $pageSize = 0, int $initialPage = 1, array $params = []): self
768
    {
769
        $this->setRemotePagination($this->TempLink('load', false), $params, $pageSize, $initialPage);
770
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/5.2/sort#ajax-sort
771
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/5.2/filter#ajax-filter
772
        return $this;
773
    }
774
775
    public function setProgressiveLoad(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0): self
776
    {
777
        $this->setOption("ajaxURL", $url);
778
        if (!empty($params)) {
779
            $this->setOption("ajaxParams", $params);
780
        }
781
        $this->setOption("progressiveLoad", $mode);
782
        if ($scrollMargin > 0) {
783
            $this->setOption("progressiveLoadScrollMargin", $scrollMargin);
784
        }
785
        if (!$pageSize) {
786
            $pageSize = $this->pageSize;
787
        }
788
        $this->setOption("paginationSize", $pageSize);
789
        $this->setOption("paginationInitialPage", $initialPage);
790
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/5.2/page#counter
791
        return $this;
792
    }
793
794
    public function wizardProgressiveLoad(int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0, array $extraParams = []): self
795
    {
796
        $params = array_merge([
797
            'SecurityID' => SecurityToken::getSecurityID()
798
        ], $extraParams);
799
        $this->setProgressiveLoad($this->TempLink('load', false), $params, $pageSize, $initialPage, $mode, $scrollMargin);
800
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/5.2/sort#ajax-sort
801
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/5.2/filter#ajax-filter
802
        return $this;
803
    }
804
805
    public function wizardResponsiveCollapse(bool $startOpen = false, string $mode = "collapse"): self
806
    {
807
        $this->setOption("responsiveLayout", $mode);
808
        $this->setOption("responsiveLayoutCollapseStartOpen", $startOpen);
809
        $this->columns = array_merge([
810
            'ui_responsive_collapse' => [
811
                "cssClass" => 'tabulator-cell-btn',
812
                'formatter' => 'responsiveCollapse',
813
                'headerSort' => false,
814
                'width' => 40,
815
            ]
816
        ], $this->columns);
817
        return $this;
818
    }
819
820
    public function wizardDataTree(bool $startExpanded = false, bool $filter = false, bool $sort = false, string $el = null): self
821
    {
822
        $this->setOption("dataTree", true);
823
        $this->setOption("dataTreeStartExpanded", $startExpanded);
824
        $this->setOption("dataTreeFilter", $filter);
825
        $this->setOption("dataTreeSort", $sort);
826
        if ($el) {
827
            $this->setOption("dataTreeElementColumn", $el);
828
        }
829
        return $this;
830
    }
831
832
    public function wizardSelectable(array $actions = []): self
833
    {
834
        $this->columns = array_merge([
835
            'ui_selectable' => [
836
                "hozAlign" => 'center',
837
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector',
838
                'formatter' => 'rowSelection',
839
                'titleFormatter' => 'rowSelection',
840
                'headerSort' => false,
841
                'width' => 40,
842
                'cellClick' => 'SSTabulator.forwardClick',
843
            ]
844
        ], $this->columns);
845
        $this->setBulkActions($actions);
846
        return $this;
847
    }
848
849
    public function wizardMoveable(string $callback = "SSTabulator.rowMoved"): self
850
    {
851
        $this->moveUrl = $this->TempLink("item/{ID}/ajaxMove", false);
852
        $this->setOption("movableRows", true);
853
        $this->addListener("rowMoved", $callback);
854
        $this->columns = array_merge([
855
            'ui_move' => [
856
                "hozAlign" => 'center',
857
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector',
858
                'rowHandle' => true,
859
                'formatter' => 'handle',
860
                'headerSort' => false,
861
                'frozen' => true,
862
                'width' => 40,
863
            ]
864
        ], $this->columns);
865
        return $this;
866
    }
867
868
    /**
869
     * @param string $field
870
     * @param string $toggleElement arrow|header|false (header by default)
871
     * @param boolean $isBool
872
     * @return void
873
     */
874
    public function wizardGroupBy(string $field, string $toggleElement = 'header', bool $isBool = false)
875
    {
876
        $this->setOption("groupBy", $field);
877
        $this->setOption("groupToggleElement", $toggleElement);
878
        if ($isBool) {
879
            $this->setOption("groupHeader", self::JS_BOOL_GROUP_HEADER);
880
        }
881
    }
882
883
    /**
884
     * @param HTTPRequest $request
885
     * @return HTTPResponse
886
     */
887
    public function handleItem($request)
888
    {
889
        // Our getController could either give us a true Controller, if this is the top-level GridField.
890
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
891
        $requestHandler = $this->getForm()->getController();
892
        $record = $this->getRecordFromRequest($request);
893
        if (!$record) {
894
            return $requestHandler->httpError(404, 'That record was not found');
895
        }
896
        $handler = $this->getItemRequestHandler($record, $requestHandler);
897
        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...
898
    }
899
900
    /**
901
     * @param HTTPRequest $request
902
     * @return HTTPResponse
903
     */
904
    public function handleTool($request)
905
    {
906
        // Our getController could either give us a true Controller, if this is the top-level GridField.
907
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
908
        $requestHandler = $this->getForm()->getController();
909
        $tool = $this->getToolFromRequest($request);
910
        if (!$tool) {
911
            return $requestHandler->httpError(404, 'That tool was not found');
912
        }
913
        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...
914
    }
915
916
    /**
917
     * @param HTTPRequest $request
918
     * @return HTTPResponse
919
     */
920
    public function handleBulkAction($request)
921
    {
922
        // Our getController could either give us a true Controller, if this is the top-level GridField.
923
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
924
        $requestHandler = $this->getForm()->getController();
925
        $bulkAction = $this->getBulkActionFromRequest($request);
926
        if (!$bulkAction) {
927
            return $requestHandler->httpError(404, 'That bulk action was not found');
928
        }
929
        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...
930
    }
931
932
    /**
933
     * @return string name of {@see TabulatorGrid_ItemRequest} subclass
934
     */
935
    public function getItemRequestClass(): string
936
    {
937
        if ($this->itemRequestClass) {
938
            return $this->itemRequestClass;
939
        } elseif (ClassInfo::exists(static::class . '_ItemRequest')) {
940
            return static::class . '_ItemRequest';
941
        }
942
        return TabulatorGrid_ItemRequest::class;
943
    }
944
945
    /**
946
     * Build a request handler for the given record
947
     *
948
     * @param DataObject $record
949
     * @param RequestHandler $requestHandler
950
     * @return TabulatorGrid_ItemRequest
951
     */
952
    protected function getItemRequestHandler($record, $requestHandler)
953
    {
954
        $class = $this->getItemRequestClass();
955
        $assignedClass = $this->itemRequestClass;
956
        $this->extend('updateItemRequestClass', $class, $record, $requestHandler, $assignedClass);
957
        /** @var TabulatorGrid_ItemRequest $handler */
958
        $handler = Injector::inst()->createWithArgs(
959
            $class,
960
            [$this, $record, $requestHandler]
961
        );
962
        if ($template = $this->getTemplate()) {
963
            $handler->setTemplate($template);
964
        }
965
        $this->extend('updateItemRequestHandler', $handler);
966
        return $handler;
967
    }
968
969
    public function getStateKey()
970
    {
971
        return $this->getName();
972
    }
973
974
    public function getState(HTTPRequest $request)
975
    {
976
        $stateKey = $this->getName();
977
        $state = $request->getSession()->get("TabulatorState[$stateKey]");
978
        return $state ?? [
979
            'page' => 1,
980
            'limit' => $this->pageSize,
981
            'sort' => [],
982
            'filter' => [],
983
        ];
984
    }
985
986
    public function setState(HTTPRequest $request, $state)
987
    {
988
        $stateKey = $this->getName();
989
        $request->getSession()->set("TabulatorState[$stateKey]", $state);
990
    }
991
992
    public function getInitScript(): string
993
    {
994
        $JsonOptions = $this->JsonOptions();
995
        $ID = $this->ID();
996
        $script = "SSTabulator.init(\"#$ID\", $JsonOptions);";
997
        return $script;
998
    }
999
1000
    /**
1001
     * Provides the configuration for this instance
1002
     *
1003
     * This is really useful in the context of the admin as it will be served over
1004
     * ajax
1005
     *
1006
     * @param HTTPRequest $request
1007
     * @return HTTPResponse
1008
     */
1009
    public function configProvider(HTTPRequest $request)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

1009
    public function configProvider(/** @scrutinizer ignore-unused */ HTTPRequest $request)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1010
    {
1011
        $response = new HTTPResponse($this->getInitScript());
1012
        $response->addHeader('Content-Type', 'application/script');
1013
        return $response;
1014
    }
1015
1016
    /**
1017
     * Provides autocomplete lists
1018
     *
1019
     * @param HTTPRequest $request
1020
     * @return HTTPResponse
1021
     */
1022
    public function autocomplete(HTTPRequest $request)
1023
    {
1024
        if ($this->isDisabled() || $this->isReadonly()) {
1025
            return $this->httpError(403);
1026
        }
1027
        $SecurityID = $request->getVar('SecurityID');
1028
        if (!SecurityToken::inst()->check($SecurityID)) {
1029
            return $this->httpError(404, "Invalid SecurityID");
1030
        }
1031
1032
        $name = $request->getVar("Column");
1033
        $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

1033
        $col = $this->getColumn(/** @scrutinizer ignore-type */ $name);
Loading history...
1034
        if (!$col) {
1035
            return $this->httpError(403, "Invalid column");
1036
        }
1037
1038
        $term = '%' . $request->getVar('term') . '%';
1039
1040
        $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

1040
        $parts = explode(".", /** @scrutinizer ignore-type */ $name);
Loading history...
1041
        if (count($parts) > 2) {
1042
            array_pop($parts);
1043
        }
1044
        if (count($parts) == 2) {
1045
            $class = $parts[0];
1046
            $field = $parts[1];
1047
        } elseif (count($parts) == 1) {
1048
            $class = preg_replace("/ID$/", "", $parts[0]);
1049
            $field = 'Title';
1050
        } else {
1051
            return $this->httpError(403, "Invalid field");
1052
        }
1053
1054
        /** @var DataObject $sng */
1055
        $sng = $class::singleton();
1056
        $baseTable = $sng->baseTable();
1057
1058
        $searchField = null;
1059
        $searchCandidates = [
1060
            $field, 'Name', 'Surname', 'Email', 'ID'
1061
        ];
1062
1063
        // Ensure field exists, this is really rudimentary
1064
        $db = $class::config()->db;
1065
        foreach ($searchCandidates as $searchCandidate) {
1066
            if ($searchField) {
1067
                continue;
1068
            }
1069
            if (isset($db[$searchCandidate])) {
1070
                $searchField = $searchCandidate;
1071
            }
1072
        }
1073
        $searchCols = [$searchField];
1074
1075
        // For members, do something better
1076
        if ($baseTable == 'Member') {
1077
            $searchField = ['FirstName', 'Surname'];
1078
            $searchCols = ['FirstName', 'Surname', 'Email'];
1079
        }
1080
1081
        if (!empty($col['editorParams']['customSearchField'])) {
1082
            $searchField = $col['editorParams']['customSearchField'];
1083
        }
1084
        if (!empty($col['editorParams']['customSearchCols'])) {
1085
            $searchCols = $col['editorParams']['customSearchCols'];
1086
        }
1087
1088
1089
        /** @var DataList $list */
1090
        $list = $sng::get();
1091
1092
        // Make sure at least one field is not null...
1093
        $where = [];
1094
        foreach ($searchCols as $searchCol) {
1095
            $where[] = $searchCol . ' IS NOT NULL';
1096
        }
1097
        $list = $list->where($where);
1098
        // ... and matches search term ...
1099
        $where = [];
1100
        foreach ($searchCols as $searchCol) {
1101
            $where[$searchCol . ' LIKE ?'] = $term;
1102
        }
1103
        $list = $list->whereAny($where);
1104
1105
        // ... and any user set requirements
1106
        if (!empty($col['editorParams']['where'])) {
1107
            // Deal with in clause
1108
            $customWhere = [];
1109
            foreach ($col['editorParams']['where'] as $col => $param) {
1110
                // For array, we need a IN statement with a ? for each value
1111
                if (is_array($param)) {
1112
                    $prepValue = [];
1113
                    $params = [];
1114
                    foreach ($param as $paramValue) {
1115
                        $params[] = $paramValue;
1116
                        $prepValue[] = "?";
1117
                    }
1118
                    $customWhere["$col IN (" . implode(',', $prepValue) . ")"] = $params;
1119
                } else {
1120
                    $customWhere["$col = ?"] = $param;
1121
                }
1122
            }
1123
            $list = $list->where($customWhere);
1124
        }
1125
1126
        $results = iterator_to_array($list);
1127
        $data = [];
1128
        foreach ($results as $record) {
1129
            if (is_array($searchField)) {
1130
                $labelParts = [];
1131
                foreach ($searchField as $sf) {
1132
                    $labelParts[] = $record->$sf;
1133
                }
1134
                $label = implode(" ", $labelParts);
1135
            } else {
1136
                $label = $record->$searchField;
1137
            }
1138
            $data[] = [
1139
                'value' => $record->ID,
1140
                'label' => $label,
1141
            ];
1142
        }
1143
1144
        $json = json_encode($data);
1145
        $response = new HTTPResponse($json);
1146
        $response->addHeader('Content-Type', 'application/script');
1147
        return $response;
1148
    }
1149
1150
    /**
1151
     * @link http://www.tabulator.info/docs/5.2/page#remote-response
1152
     * @param HTTPRequest $request
1153
     * @return HTTPResponse
1154
     */
1155
    public function load(HTTPRequest $request)
1156
    {
1157
        if ($this->isDisabled() || $this->isReadonly()) {
1158
            return $this->httpError(403);
1159
        }
1160
        $SecurityID = $request->getVar('SecurityID');
1161
        if (!SecurityToken::inst()->check($SecurityID)) {
1162
            return $this->httpError(404, "Invalid SecurityID");
1163
        }
1164
1165
        $page = (int) $request->getVar('page');
1166
        $limit = (int) $request->getVar('size');
1167
1168
        $sort = $request->getVar('sort');
1169
        $filter = $request->getVar('filter');
1170
1171
        // Persist state to allow the ItemEditForm to display navigation
1172
        $state = [
1173
            'page' => $page,
1174
            'limit' => $limit,
1175
            'sort' => $sort,
1176
            'filter' => $filter,
1177
        ];
1178
        $this->setState($request, $state);
1179
1180
        $offset = ($page - 1) * $limit;
1181
        $data = $this->getManipulatedData($limit, $offset, $sort, $filter);
1182
1183
        $response = new HTTPResponse(json_encode($data));
1184
        $response->addHeader('Content-Type', 'application/json');
1185
        return $response;
1186
    }
1187
1188
    /**
1189
     * @param HTTPRequest $request
1190
     * @return DataObject|null
1191
     */
1192
    protected function getRecordFromRequest(HTTPRequest $request): ?DataObject
1193
    {
1194
        /** @var DataObject $record */
1195
        if (is_numeric($request->param('ID'))) {
1196
            /** @var Filterable $dataList */
1197
            $dataList = $this->getList();
1198
            $record = $dataList->byID($request->param('ID'));
1199
        } else {
1200
            $record = Injector::inst()->create($this->getModelClass());
1201
        }
1202
        return $record;
1203
    }
1204
1205
    /**
1206
     * @param HTTPRequest $request
1207
     * @return AbstractTabulatorTool|null
1208
     */
1209
    protected function getToolFromRequest(HTTPRequest $request): ?AbstractTabulatorTool
1210
    {
1211
        $toolID = $request->param('ID');
1212
        $tool = $this->getTool($toolID);
1213
        return $tool;
1214
    }
1215
1216
    /**
1217
     * @param HTTPRequest $request
1218
     * @return AbstractBulkAction|null
1219
     */
1220
    protected function getBulkActionFromRequest(HTTPRequest $request): ?AbstractBulkAction
1221
    {
1222
        $toolID = $request->param('ID');
1223
        $tool = $this->getBulkAction($toolID);
1224
        return $tool;
1225
    }
1226
1227
    public function getList(): SS_List
1228
    {
1229
        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...
1230
    }
1231
1232
    public function setList(SS_List $list): self
1233
    {
1234
        if ($this->autoloadDataList && $list instanceof DataList) {
1235
            $this->wizardRemotePagination();
1236
        }
1237
        $this->list = $list;
1238
        return $this;
1239
    }
1240
1241
    public function hasArrayList(): bool
1242
    {
1243
        return $this->list instanceof ArrayList;
1244
    }
1245
1246
    public function getArrayList(): ArrayList
1247
    {
1248
        if (!$this->list instanceof ArrayList) {
1249
            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

1249
            throw new RuntimeException("Value is not a ArrayList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1250
        }
1251
        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...
1252
    }
1253
1254
    public function hasDataList(): bool
1255
    {
1256
        return $this->list instanceof DataList;
1257
    }
1258
1259
    public function getDataList(): DataList
1260
    {
1261
        if (!$this->list instanceof DataList) {
1262
            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

1262
            throw new RuntimeException("Value is not a DataList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1263
        }
1264
        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...
1265
    }
1266
1267
    public function getManipulatedData(int $limit, int $offset, array $sort = null, array $filter = null): array
1268
    {
1269
        if (!$this->hasDataList()) {
1270
            $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

1270
            /** @scrutinizer ignore-call */ 
1271
            $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...
1271
1272
            $lastRow = $this->list->count();
1273
            $lastPage = ceil($lastRow / $limit);
1274
1275
            $result = [
1276
                'last_row' => $lastRow,
1277
                'last_page' => $lastPage,
1278
                'data' => $data,
1279
            ];
1280
1281
            return $result;
1282
        }
1283
1284
        $dataList = $this->getDataList();
1285
1286
        $schema = DataObject::getSchema();
1287
        $dataClass = $dataList->dataClass();
1288
        /** @var DataObject $singleton */
1289
        $singleton = singleton($dataClass);
1290
        $resolutionMap = [];
1291
1292
        $sortSql = [];
1293
        if ($sort) {
1294
            foreach ($sort as $sortValues) {
1295
                $cols = array_keys($this->columns);
1296
                $field = $sortValues['field'];
1297
                if (!in_array($field, $cols)) {
1298
                    throw new Exception("Invalid sort field: $field");
1299
                }
1300
                $dir = $sortValues['dir'];
1301
                if (!in_array($dir, ['asc', 'desc'])) {
1302
                    throw new Exception("Invalid sort dir: $dir");
1303
                }
1304
1305
                // Nested sort
1306
                if (strpos($field, '.') !== false) {
1307
                    $parts = explode(".", $field);
1308
                    if (!isset($resolutionMap[$parts[0]])) {
1309
                        $resolutionMap[$parts[0]] = singleton($dataClass)->relObject($parts[0]);
1310
                    }
1311
                    $relatedObject = get_class($resolutionMap[$parts[0]]);
1312
                    $tableName = $schema->tableForField($relatedObject, $parts[1]);
1313
                    $baseIDColumn = $schema->sqlColumnForField($dataClass, 'ID');
1314
                    $tableAlias = $parts[0];
1315
                    $dataList = $dataList->leftJoin($tableName, "\"{$tableAlias}\".\"ID\" = {$baseIDColumn}", $tableAlias);
1316
                }
1317
1318
                $sortSql[] = $field . ' ' . $dir;
1319
            }
1320
        }
1321
        if (!empty($sortSql)) {
1322
            $dataList = $dataList->sort(implode(", ", $sortSql));
1323
        }
1324
1325
        // Filtering is an array of field/type/value arrays
1326
        $where = [];
1327
        if ($filter) {
1328
            foreach ($filter as $filterValues) {
1329
                $cols = array_keys($this->columns);
1330
                $field = $filterValues['field'];
1331
                if (!in_array($field, $cols)) {
1332
                    throw new Exception("Invalid sort field: $field");
1333
                }
1334
                $value = $filterValues['value'];
1335
                $type = $filterValues['type'];
1336
1337
                // Strict value
1338
                if ($value === "true") {
1339
                    $value = true;
1340
                } elseif ($value === "false") {
1341
                    $value = false;
1342
                }
1343
1344
                switch ($type) {
1345
                    case "=":
1346
                        $where["$field"] = $value;
1347
                        break;
1348
                    case "!=":
1349
                        $where["$field:not"] = $value;
1350
                        break;
1351
                    case "like":
1352
                        $where["$field:PartialMatch:nocase"] = $value;
1353
                        break;
1354
                    case "keywords":
1355
                        $where["$field:PartialMatch:nocase"] = str_replace(" ", "%", $value);
1356
                        break;
1357
                    case "starts":
1358
                        $where["$field:StartsWith:nocase"] = $value;
1359
                        break;
1360
                    case "ends":
1361
                        $where["$field:EndsWith:nocase"] = $value;
1362
                        break;
1363
                    case "<":
1364
                        $where["$field:LessThan:nocase"] = $value;
1365
                        break;
1366
                    case "<=":
1367
                        $where["$field:LessThanOrEqual:nocase"] = $value;
1368
                        break;
1369
                    case ">":
1370
                        $where["$field:GreaterThan:nocase"] = $value;
1371
                        break;
1372
                    case ">=":
1373
                        $where["$field:GreaterThanOrEqual:nocase"] = $value;
1374
                        break;
1375
                    case "in":
1376
                        $where["$field"] = $value;
1377
                        break;
1378
                    case "regex":
1379
                        $dataList = $dataList->where('REGEXP ' . Convert::raw2sql($value));
1380
                        break;
1381
                    default:
1382
                        throw new Exception("Invalid sort dir: $dir");
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $dir does not seem to be defined for all execution paths leading up to this point.
Loading history...
1383
                }
1384
            }
1385
        }
1386
        if (!empty($where)) {
1387
            $dataList = $dataList->filter($where);
1388
        }
1389
1390
        $lastRow = $dataList->count();
1391
        $lastPage = ceil($lastRow / $limit);
1392
1393
        $data = [];
1394
        /** @var DataObject $record */
1395
        foreach ($dataList->limit($limit, $offset) as $record) {
1396
            if ($record->hasMethod('canView') && !$record->canView()) {
1397
                continue;
1398
            }
1399
1400
            $item = [
1401
                'ID' => $record->ID,
1402
            ];
1403
            $nested = [];
1404
            foreach ($this->columns as $col) {
1405
                $field = $col['field'] ?? null; // actions don't have field
1406
                if (strpos($field, '.') !== false) {
0 ignored issues
show
Bug introduced by
It seems like $field can also be of type null; however, parameter $haystack of strpos() 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

1406
                if (strpos(/** @scrutinizer ignore-type */ $field, '.') !== false) {
Loading history...
1407
                    $parts = explode('.', $field);
0 ignored issues
show
Bug introduced by
It seems like $field 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

1407
                    $parts = explode('.', /** @scrutinizer ignore-type */ $field);
Loading history...
1408
                    if ($singleton->getRelationClass($parts[0])) {
1409
                        $nested[$parts[0]][] = $parts[1];
1410
                        continue;
1411
                    }
1412
                }
1413
                $item[$field] = $record->getField($field);
1414
            }
1415
            foreach ($nested as $nestedClass => $nestedColumns) {
1416
                $relObject = $record->relObject($nestedClass);
1417
                $nestedData = [];
1418
                foreach ($nestedColumns as $nestedColumn) {
1419
                    $nestedData[$nestedColumn] = $relObject->getField($nestedColumn);
1420
                }
1421
                $item[$nestedClass] = $nestedData;
1422
            }
1423
            $data[] = $item;
1424
        }
1425
1426
        $result = [
1427
            'last_row' => $lastRow,
1428
            'last_page' => $lastPage,
1429
            'data' => $data,
1430
        ];
1431
1432
        return $result;
1433
    }
1434
1435
    public function getModelClass(): ?string
1436
    {
1437
        if ($this->modelClass) {
1438
            return $this->modelClass;
1439
        }
1440
        if ($this->list && $this->list instanceof DataList) {
1441
            return $this->list->dataClass();
1442
        }
1443
        return null;
1444
    }
1445
1446
    public function setModelClass(string $modelClass): self
1447
    {
1448
        $this->modelClass = $modelClass;
1449
        return $this;
1450
    }
1451
1452
1453
    public function getDataAttribute(string $k)
1454
    {
1455
        if (isset($this->dataAttributes[$k])) {
1456
            return $this->dataAttributes[$k];
1457
        }
1458
        return $this->getAttribute("data-$k");
1459
    }
1460
1461
    public function setDataAttribute(string $k, $v): self
1462
    {
1463
        $this->dataAttributes[$k] = $v;
1464
        return $this;
1465
    }
1466
1467
    public function dataAttributesHTML(): string
1468
    {
1469
        $parts = [];
1470
        foreach ($this->dataAttributes as $k => $v) {
1471
            if (!$v) {
1472
                continue;
1473
            }
1474
            if (is_array($v)) {
1475
                $v = json_encode($v);
1476
            }
1477
            $parts[] = "data-$k='$v'";
1478
        }
1479
        return implode(" ", $parts);
1480
    }
1481
1482
    protected function processLink(string $url): string
1483
    {
1484
        // It's not necessary to process
1485
        if ($url == '#') {
1486
            return $url;
1487
        }
1488
        // It's a temporary link on the form
1489
        if (strpos($url, 'form:') === 0) {
1490
            return $this->Link(preg_replace('/^form:/', '', $url));
1491
        }
1492
        // It's a temporary link on the controller
1493
        if (strpos($url, 'controller:') === 0) {
1494
            return $this->ControllerLink(preg_replace('/^controller:/', '', $url));
1495
        }
1496
        // It's a custom protocol (mailto: etc)
1497
        if (strpos($url, ':') !== false) {
1498
            return $url;
1499
        }
1500
        return $url;
1501
    }
1502
1503
    protected function processLinks(): void
1504
    {
1505
        // Process editor and formatter links
1506
        foreach ($this->columns as $name => $params) {
1507
            if (!empty($params['formatterParams']['url'])) {
1508
                $url = $this->processLink($params['formatterParams']['url']);
1509
                $this->columns[$name]['formatterParams']['url'] = $url;
1510
            }
1511
            if (!empty($params['editorParams']['url'])) {
1512
                $url = $this->processLink($params['editorParams']['url']);
1513
                $this->columns[$name]['editorParams']['url'] = $url;
1514
            }
1515
            // Set valuesURL automatically if not already set
1516
            if (!empty($params['editorParams']['autocomplete'])) {
1517
                if (empty($params['editorParams']['valuesURL'])) {
1518
                    $params = [
1519
                        'Column' => $name,
1520
                        'SecurityID' => SecurityToken::getSecurityID(),
1521
                    ];
1522
                    $url = $this->Link('autocomplete') . '?' . http_build_query($params);
1523
                    $this->columns[$name]['editorParams']['valuesURL'] = $url;
1524
                    $this->columns[$name]['editorParams']['filterRemote'] = true;
1525
                }
1526
            }
1527
        }
1528
1529
        // Other links
1530
        $url = $this->getOption('ajaxURL');
1531
        if ($url) {
1532
            $this->setOption('ajaxURL', $this->processLink($url));
1533
        }
1534
    }
1535
1536
    public function makeButton(string $urlOrAction, string $icon, string $title): array
1537
    {
1538
        $opts = [
1539
            "responsive" => 0,
1540
            "cssClass" => 'tabulator-cell-btn',
1541
            "tooltip" => $title,
1542
            "formatter" => "SSTabulator.buttonFormatter",
1543
            "formatterParams" => [
1544
                "icon" => $icon,
1545
                "title" => $title,
1546
                "url" => $this->TempLink($urlOrAction), // On the controller by default
1547
            ],
1548
            "cellClick" => "SSTabulator.buttonHandler",
1549
            "width" => 70,
1550
            "hozAlign" => "center",
1551
            "headerSort" => false,
1552
1553
        ];
1554
        return $opts;
1555
    }
1556
1557
    public function addButtonFromArray(string $action, array $opts = [], string $before = null): self
1558
    {
1559
        // Insert before given column
1560
        if ($before) {
1561
            if (array_key_exists($before, $this->columns)) {
1562
                $new = [];
1563
                foreach ($this->columns as $k => $value) {
1564
                    if ($k === $before) {
1565
                        $new["action_$action"] = $opts;
1566
                    }
1567
                    $new[$k] = $value;
1568
                }
1569
                $this->columns = $new;
1570
            }
1571
        } else {
1572
            $this->columns["action_$action"] = $opts;
1573
        }
1574
        return $this;
1575
    }
1576
1577
    /**
1578
     * @param string $action Action name
1579
     * @param string $url Parameters between {} will be interpolated by row values.
1580
     * @param string $icon
1581
     * @param string $title
1582
     * @param string|null $before
1583
     * @return self
1584
     */
1585
    public function addButton(string $action, string $url, string $icon, string $title, string $before = null): self
1586
    {
1587
        $opts = $this->makeButton($url, $icon, $title);
1588
        $this->addButtonFromArray($action, $opts, $before);
1589
        return $this;
1590
    }
1591
1592
    public function shiftButton(string $action, string $url, string $icon, string $title): self
1593
    {
1594
        // Find first action
1595
        foreach ($this->columns as $name => $options) {
1596
            if (strpos($name, 'action_') === 0) {
1597
                return $this->addButton($action, $url, $icon, $title, $name);
1598
            }
1599
        }
1600
        return $this->addButton($action, $url, $icon, $title);
1601
    }
1602
1603
    public function removeButton(string $action): self
1604
    {
1605
        if (isset($this->columns["action_$action"])) {
1606
            unset($this->columns["action_$action"]);
1607
        }
1608
        return $this;
1609
    }
1610
1611
    /**
1612
     * @link http://www.tabulator.info/docs/5.2/columns#definition
1613
     * @param string $field (Required) this is the key for this column in the data array
1614
     * @param string $title (Required) This is the title that will be displayed in the header for this column
1615
     * @param array $opts Other options to merge in
1616
     */
1617
    public function addColumn(string $field, string $title = null, array $opts = []): self
1618
    {
1619
        if ($title === null) {
1620
            $title = $field;
1621
        }
1622
1623
        $baseOpts = [
1624
            "field" => $field,
1625
            "title" => $title,
1626
        ];
1627
1628
        if (!empty($opts)) {
1629
            $baseOpts = array_merge($baseOpts, $opts);
1630
        }
1631
1632
        $this->columns[$field] = $baseOpts;
1633
        return $this;
1634
    }
1635
1636
    public function makeColumnEditable(string $field, string $editor = "input", array $params = [])
1637
    {
1638
        $col = $this->getColumn($field);
1639
        if (!$col) {
1640
            throw new InvalidArgumentException("$field is not a valid column");
1641
        }
1642
1643
        switch ($editor) {
1644
            case 'date':
1645
                $editor = "input";
1646
                $params = [
1647
                    'mask' => "9999-99-99",
1648
                    'maskAutoFill' => 'true',
1649
                ];
1650
                break;
1651
            case 'datetime':
1652
                $editor = "input";
1653
                $params = [
1654
                    'mask' => "9999-99-99 99:99:99",
1655
                    'maskAutoFill' => 'true',
1656
                ];
1657
                break;
1658
        }
1659
1660
        if (empty($col['cssClass'])) {
1661
            $col['cssClass'] = 'no-change-track';
1662
        } else {
1663
            $col['cssClass'] .= ' no-change-track';
1664
        }
1665
1666
        $col['editor'] = $editor;
1667
        $col['editorParams'] = $params;
1668
        if ($editor == "list") {
1669
            if (!empty($params['autocomplete'])) {
1670
                $col['headerFilter'] = "input"; // force input
1671
            } else {
1672
                $col['headerFilterParams'] = $params; // editor is used as base filter editor
1673
            }
1674
        }
1675
1676
1677
        $this->setColumn($field, $col);
1678
    }
1679
1680
    /**
1681
     * Get column details
1682
1683
     * @param string $key
1684
     */
1685
    public function getColumn(string $key): ?array
1686
    {
1687
        if (isset($this->columns[$key])) {
1688
            return $this->columns[$key];
1689
        }
1690
        return null;
1691
    }
1692
1693
    /**
1694
     * Set column details
1695
     *
1696
     * @param string $key
1697
     * @param array $col
1698
     */
1699
    public function setColumn(string $key, array $col): self
1700
    {
1701
        $this->columns[$key] = $col;
1702
        return $this;
1703
    }
1704
1705
    /**
1706
     * Remove a column
1707
     *
1708
     * @param string $key
1709
     */
1710
    public function removeColumn(string $key): void
1711
    {
1712
        unset($this->columns[$key]);
1713
    }
1714
1715
1716
    /**
1717
     * Get the value of columns
1718
     */
1719
    public function getColumns(): array
1720
    {
1721
        return $this->columns;
1722
    }
1723
1724
    /**
1725
     * Set the value of columns
1726
     */
1727
    public function setColumns(array $columns): self
1728
    {
1729
        $this->columns = $columns;
1730
        return $this;
1731
    }
1732
1733
    /**
1734
     * @param string|AbstractTabulatorTool $tool Pass name or class
1735
     * @return AbstractTabulatorTool|null
1736
     */
1737
    public function getTool($tool): ?AbstractTabulatorTool
1738
    {
1739
        if (is_object($tool)) {
1740
            $tool = get_class($tool);
1741
        }
1742
        if (!is_string($tool)) {
0 ignored issues
show
introduced by
The condition is_string($tool) is always true.
Loading history...
1743
            throw new InvalidArgumentException('Tool must be an object or a class name');
1744
        }
1745
        foreach ($this->tools as $t) {
1746
            if ($t['name'] === $tool) {
1747
                return $t['tool'];
1748
            }
1749
            if ($t['tool'] instanceof $tool) {
1750
                return $t['tool'];
1751
            }
1752
        }
1753
        return null;
1754
    }
1755
1756
    public function addTool(string $pos, AbstractTabulatorTool $tool, string $name = ''): self
1757
    {
1758
        $tool->setTabulatorGrid($this);
1759
1760
        $this->tools[] = [
1761
            'position' => $pos,
1762
            'tool' => $tool,
1763
            'name' => $name,
1764
        ];
1765
        return $this;
1766
    }
1767
1768
    public function removeTool($tool): self
1769
    {
1770
        if (is_object($tool)) {
1771
            $tool = get_class($tool);
1772
        }
1773
        if (!is_string($tool)) {
1774
            throw new InvalidArgumentException('Tool must be an object or a class name');
1775
        }
1776
        foreach ($this->tools as $idx => $tool) {
0 ignored issues
show
introduced by
$tool is overwriting one of the parameters of this function.
Loading history...
1777
            if ($tool['name'] === $tool) {
1778
                unset($this->tools[$idx]);
1779
            }
1780
            if ($tool['tool'] instanceof $tool) {
1781
                unset($this->tools[$idx]);
1782
            }
1783
        }
1784
        return $this;
1785
    }
1786
1787
    /**
1788
     * @param string|AbstractBulkAction $bulkAction Pass name or class
1789
     * @return AbstractBulkAction|null
1790
     */
1791
    public function getBulkAction($bulkAction): ?AbstractBulkAction
1792
    {
1793
        if (is_object($bulkAction)) {
1794
            $bulkAction = get_class($bulkAction);
1795
        }
1796
        if (!is_string($bulkAction)) {
0 ignored issues
show
introduced by
The condition is_string($bulkAction) is always true.
Loading history...
1797
            throw new InvalidArgumentException('BulkAction must be an object or a class name');
1798
        }
1799
        foreach ($this->bulkActions as $ba) {
1800
            if ($ba->getName() == $bulkAction) {
1801
                return $ba;
1802
            }
1803
            if ($ba instanceof $bulkAction) {
1804
                return $ba;
1805
            }
1806
        }
1807
        return null;
1808
    }
1809
1810
    public function getBulkActions(): array
1811
    {
1812
        return $this->bulkActions;
1813
    }
1814
1815
    /**
1816
     * @param AbstractBulkAction[] $bulkActions
1817
     * @return self
1818
     */
1819
    public function setBulkActions(array $bulkActions): self
1820
    {
1821
        foreach ($bulkActions as $bulkAction) {
1822
            $bulkAction->setTabulatorGrid($this);
1823
        }
1824
        $this->bulkActions = $bulkActions;
1825
        return $this;
1826
    }
1827
1828
    public function addBulkAction(AbstractBulkAction $handler): self
1829
    {
1830
        $handler->setTabulatorGrid($this);
1831
1832
        $this->bulkActions[] = $handler;
1833
        return $this;
1834
    }
1835
1836
    public function removeBulkAction($bulkAction): self
1837
    {
1838
        if (is_object($bulkAction)) {
1839
            $bulkAction = get_class($bulkAction);
1840
        }
1841
        if (!is_string($bulkAction)) {
1842
            throw new InvalidArgumentException('Bulk action must be an object or a class name');
1843
        }
1844
        foreach ($this->bulkActions as $idx => $ba) {
1845
            if ($ba->getName() == $bulkAction) {
1846
                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...
1847
            }
1848
            if ($ba instanceof $bulkAction) {
1849
                unset($this->bulkAction[$idx]);
1850
            }
1851
        }
1852
        return $this;
1853
    }
1854
1855
    public function getColumnDefault(string $opt)
1856
    {
1857
        return $this->columnDefaults[$opt] ?? null;
1858
    }
1859
1860
    public function setColumnDefault(string $opt, $value)
1861
    {
1862
        $this->columnDefaults[$opt] = $value;
1863
    }
1864
1865
    public function getColumnDefaults(): array
1866
    {
1867
        return $this->columnDefaults;
1868
    }
1869
1870
    public function setColumnDefaults(array $columnDefaults): self
1871
    {
1872
        $this->columnDefaults = $columnDefaults;
1873
        return $this;
1874
    }
1875
1876
    public function getListeners(): array
1877
    {
1878
        return $this->listeners;
1879
    }
1880
1881
    public function setListeners(array $listeners): self
1882
    {
1883
        $this->listeners = $listeners;
1884
        return $this;
1885
    }
1886
1887
    public function addListener(string $event, string $functionName): self
1888
    {
1889
        $this->listeners[$event] = $functionName;
1890
        return $this;
1891
    }
1892
1893
    public function removeListener(string $event): self
1894
    {
1895
        if (isset($this->listeners[$event])) {
1896
            unset($this->listeners[$event]);
1897
        }
1898
        return $this;
1899
    }
1900
1901
    public function getJsNamespaces(): array
1902
    {
1903
        return $this->jsNamespaces;
1904
    }
1905
1906
    public function setJsNamespaces(array $jsNamespaces): self
1907
    {
1908
        $this->jsNamespaces = $jsNamespaces;
1909
        return $this;
1910
    }
1911
1912
    public function registerJsNamespace(string $ns): self
1913
    {
1914
        $this->jsNamespaces[] = $ns;
1915
        return $this;
1916
    }
1917
1918
    public function unregisterJsNamespace(string $ns): self
1919
    {
1920
        $this->jsNamespaces = array_diff($this->jsNamespaces, [$ns]);
1921
        return $this;
1922
    }
1923
1924
    public function getLinksOptions(): array
1925
    {
1926
        return $this->linksOptions;
1927
    }
1928
1929
    public function setLinksOptions(array $linksOptions): self
1930
    {
1931
        $this->linksOptions = $linksOptions;
1932
        return $this;
1933
    }
1934
1935
    public function registerLinkOption(string $linksOption): self
1936
    {
1937
        $this->linksOptions[] = $linksOption;
1938
        return $this;
1939
    }
1940
1941
    public function unregisterLinkOption(string $linksOption): self
1942
    {
1943
        $this->linksOptions = array_diff($this->linksOptions, [$linksOption]);
1944
        return $this;
1945
    }
1946
1947
    /**
1948
     * Get the value of pageSize
1949
     */
1950
    public function getPageSize(): int
1951
    {
1952
        return $this->pageSize;
1953
    }
1954
1955
    /**
1956
     * Set the value of pageSize
1957
     *
1958
     * @param int $pageSize
1959
     */
1960
    public function setPageSize(int $pageSize): self
1961
    {
1962
        $this->pageSize = $pageSize;
1963
        return $this;
1964
    }
1965
1966
    /**
1967
     * Get the value of autoloadDataList
1968
     */
1969
    public function getAutoloadDataList(): bool
1970
    {
1971
        return $this->autoloadDataList;
1972
    }
1973
1974
    /**
1975
     * Set the value of autoloadDataList
1976
     *
1977
     * @param bool $autoloadDataList
1978
     */
1979
    public function setAutoloadDataList(bool $autoloadDataList): self
1980
    {
1981
        $this->autoloadDataList = $autoloadDataList;
1982
        return $this;
1983
    }
1984
1985
    /**
1986
     * Set the value of itemRequestClass
1987
     */
1988
    public function setItemRequestClass(string $itemRequestClass): self
1989
    {
1990
        $this->itemRequestClass = $itemRequestClass;
1991
        return $this;
1992
    }
1993
1994
    /**
1995
     * Get the value of lazyInit
1996
     */
1997
    public function getLazyInit(): bool
1998
    {
1999
        return $this->lazyInit;
2000
    }
2001
2002
    /**
2003
     * Set the value of lazyInit
2004
     */
2005
    public function setLazyInit(bool $lazyInit): self
2006
    {
2007
        $this->lazyInit = $lazyInit;
2008
        return $this;
2009
    }
2010
2011
    /**
2012
     * Get the value of rowClickTriggersAction
2013
     */
2014
    public function getRowClickTriggersAction(): bool
2015
    {
2016
        return $this->rowClickTriggersAction;
2017
    }
2018
2019
    /**
2020
     * Set the value of rowClickTriggersAction
2021
     */
2022
    public function setRowClickTriggersAction(bool $rowClickTriggersAction): self
2023
    {
2024
        $this->rowClickTriggersAction = $rowClickTriggersAction;
2025
        return $this;
2026
    }
2027
2028
    /**
2029
     * Get the value of controllerFunction
2030
     */
2031
    public function getControllerFunction(): string
2032
    {
2033
        if (!$this->controllerFunction) {
2034
            return $this->getName() ?? "TabulatorGrid";
2035
        }
2036
        return $this->controllerFunction;
2037
    }
2038
2039
    /**
2040
     * Set the value of controllerFunction
2041
     */
2042
    public function setControllerFunction(string $controllerFunction): self
2043
    {
2044
        $this->controllerFunction = $controllerFunction;
2045
        return $this;
2046
    }
2047
2048
    /**
2049
     * Get the value of useConfigProvider
2050
     */
2051
    public function getUseConfigProvider(): bool
2052
    {
2053
        return $this->useConfigProvider;
2054
    }
2055
2056
    /**
2057
     * Set the value of useConfigProvider
2058
     */
2059
    public function setUseConfigProvider(bool $useConfigProvider): self
2060
    {
2061
        $this->useConfigProvider = $useConfigProvider;
2062
        return $this;
2063
    }
2064
2065
    /**
2066
     * Get the value of editUrl
2067
     */
2068
    public function getEditUrl(): string
2069
    {
2070
        return $this->editUrl;
2071
    }
2072
2073
    /**
2074
     * Set the value of editUrl
2075
     */
2076
    public function setEditUrl(string $editUrl): self
2077
    {
2078
        $this->editUrl = $editUrl;
2079
        return $this;
2080
    }
2081
2082
    /**
2083
     * Get the value of moveUrl
2084
     */
2085
    public function getMoveUrl(): string
2086
    {
2087
        return $this->moveUrl;
2088
    }
2089
2090
    /**
2091
     * Set the value of moveUrl
2092
     */
2093
    public function setMoveUrl(string $moveUrl): self
2094
    {
2095
        $this->moveUrl = $moveUrl;
2096
        return $this;
2097
    }
2098
2099
    /**
2100
     * Get the value of bulkUrl
2101
     */
2102
    public function getBulkUrl(): string
2103
    {
2104
        return $this->bulkUrl;
2105
    }
2106
2107
    /**
2108
     * Set the value of bulkUrl
2109
     */
2110
    public function setBulkUrl(string $bulkUrl): self
2111
    {
2112
        $this->bulkUrl = $bulkUrl;
2113
        return $this;
2114
    }
2115
}
2116