Passed
Push — master ( 7b5aa0...a4bbb9 )
by Thomas
12:21
created

TabulatorGrid::getUseInitScript()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

441
            $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...
442
        }
443
444
        // - Custom actions are forwarded to the model itself
445
        if ($singl->hasMethod('tabulatorRowActions')) {
446
            $rowActions = $singl->tabulatorRowActions();
447
            if (!is_array($rowActions)) {
448
                throw new RuntimeException("tabulatorRowActions must return an array");
449
            }
450
            foreach ($rowActions as $key => $actionConfig) {
451
                $action = $actionConfig['action'] ?? $key;
452
                $url = $this->TempLink("item/{ID}/customAction/$action", false);
453
                $icon = $actionConfig['icon'] ?? "cog";
454
                $title = $actionConfig['title'] ?? "";
455
456
                $button = $this->makeButton($url, $icon, $title);
457
                if (!empty($actionConfig['ajax'])) {
458
                    $button['formatterParams']['ajax'] = true;
459
                }
460
                $this->addButtonFromArray("ui_customaction_$action", $button);
461
            }
462
        }
463
464
        $this->setRowClickTriggersAction(true);
465
    }
466
467
    public static function requirements(): void
468
    {
469
        $use_cdn = self::config()->use_cdn;
470
        $use_custom_build = self::config()->use_custom_build;
471
        $theme = self::config()->theme; // simple, midnight, modern or framework
472
        $version = self::config()->version;
473
        $luxon_version = self::config()->luxon_version;
474
        $enable_luxon = self::config()->enable_luxon;
475
        $last_icon_version = self::config()->last_icon_version;
476
        $enable_last_icon = self::config()->enable_last_icon;
477
478
        if ($use_cdn) {
479
            $baseDir = "https://cdn.jsdelivr.net/npm/tabulator-tables@$version/dist";
480
        } else {
481
            $asset = ModuleResourceLoader::resourceURL('lekoala/silverstripe-tabulator:client/cdn/js/tabulator.min.js');
482
            $baseDir = dirname(dirname($asset));
483
        }
484
485
        if ($luxon_version && $enable_luxon) {
486
            Requirements::javascript("https://cdn.jsdelivr.net/npm/luxon@$luxon_version/build/global/luxon.min.js");
487
        }
488
        if ($last_icon_version && $enable_last_icon) {
489
            Requirements::css("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.css");
490
            Requirements::javascript("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.js");
491
        }
492
        if ($use_custom_build) {
493
            // if (Director::isDev() && !Director::is_ajax()) {
494
            //     Requirements::javascript("lekoala/silverstripe-tabulator:client/custom-tabulator.js");
495
            // } else {
496
            Requirements::javascript("lekoala/silverstripe-tabulator:client/custom-tabulator.min.js");
497
            // }
498
            Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js');
499
        } else {
500
            Requirements::javascript("$baseDir/js/tabulator.min.js");
501
            Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js');
502
        }
503
504
        if ($theme) {
505
            Requirements::css("$baseDir/css/tabulator_$theme.min.css");
506
        } else {
507
            Requirements::css("$baseDir/css/tabulator.min.css");
508
        }
509
510
        if ($theme && $theme == "bootstrap5") {
511
            Requirements::css('lekoala/silverstripe-tabulator:client/custom-tabulator.min.css');
512
        }
513
    }
514
515
    public function setValue($value, $data = null)
516
    {
517
        if ($value instanceof DataList) {
518
            $this->configureFromDataObject($value->dataClass());
519
        }
520
        return parent::setValue($value, $data);
521
    }
522
523
    public function getModularName()
524
    {
525
        return 'SSTabulator.createTabulator';
526
    }
527
528
    public function getModularSelector()
529
    {
530
        return '.tabulatorgrid';
531
    }
532
533
    public function getModularConfigName()
534
    {
535
        return str_replace('-', '_', $this->ID()) . '_config';
536
    }
537
538
    public function getModularConfig()
539
    {
540
        $JsonOptions = $this->JsonOptions();
541
        $configName = $this->getModularConfigName();
542
        $script = "var $configName = $JsonOptions";
543
        return $script;
544
    }
545
546
    public function Field($properties = [])
547
    {
548
        $this->addExtraClass(self::config()->theme);
549
        if ($this->lazyInit) {
550
            $this->setModularLazy($this->lazyInit);
551
        }
552
        if (self::config()->enable_requirements) {
553
            self::requirements();
554
        }
555
556
        // Make sure we can use a standalone version of the field without a form
557
        // Function should match the name
558
        if (!$this->form) {
559
            $this->form = new Form(Controller::curr(), $this->getControllerFunction());
560
        }
561
562
        // Data attributes for our custom behaviour
563
        $this->setDataAttribute("row-click-triggers-action", $this->rowClickTriggersAction);
564
        $customIcons = self::config()->custom_pagination_icons;
565
        $this->setDataAttribute("use-custom-pagination-icons", empty($customIcons));
566
567
        $this->setDataAttribute("listeners", $this->listeners);
568
        if ($this->editUrl) {
569
            $url = $this->processLink($this->editUrl);
570
            $this->setDataAttribute("edit-url", $url);
571
        }
572
        if ($this->moveUrl) {
573
            $url = $this->processLink($this->moveUrl);
574
            $this->setDataAttribute("move-url", $url);
575
        }
576
        if (!empty($this->bulkActions)) {
577
            $url = $this->processLink($this->bulkUrl);
578
            $this->setDataAttribute("bulk-url", $url);
579
        }
580
581
        if ($this->useConfigProvider) {
582
            $configLink = "/" . ltrim($this->Link("configProvider"), "/");
583
            $configLink .= "?t=" . time();
584
            // This cannot be loaded as a js module
585
            Requirements::javascript($configLink, ['type' => 'application/javascript', 'defer' => 'true']);
586
        } elseif ($this->useInitScript) {
587
            Requirements::customScript($this->getInitScript());
0 ignored issues
show
Deprecated Code introduced by
The function LeKoala\Tabulator\TabulatorGrid::getInitScript() has been deprecated. ( Ignorable by Annotation )

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

587
            Requirements::customScript(/** @scrutinizer ignore-deprecated */ $this->getInitScript());
Loading history...
588
        }
589
590
        // Skip modular behaviour
591
        if ($this->useConfigProvider || $this->useInitScript) {
592
            return FormField::Field($properties);
593
        }
594
        return parent::Field($properties);
595
    }
596
597
    public function ShowTools(): string
598
    {
599
        if (empty($this->tools)) {
600
            return '';
601
        }
602
        $html = '';
603
        $html .= '<div class="tabulator-tools">';
604
        $html .= '<div class="tabulator-tools-start">';
605
        foreach ($this->tools as $tool) {
606
            if ($tool['position'] != self::POS_START) {
607
                continue;
608
            }
609
            $html .= ($tool['tool'])->forTemplate();
610
        }
611
        $html .= '</div>';
612
        $html .= '<div class="tabulator-tools-end">';
613
        foreach ($this->tools as $tool) {
614
            if ($tool['position'] != self::POS_END) {
615
                continue;
616
            }
617
            $html .= ($tool['tool'])->forTemplate();
618
        }
619
        // Show bulk actions at the end
620
        if (!empty($this->bulkActions)) {
621
            $selectLabel = _t(__CLASS__ . ".BULKSELECT", "Select a bulk action");
622
            $confirmLabel = _t(__CLASS__ . ".BULKCONFIRM", "Go");
623
            $html .= "<select class=\"tabulator-bulk-select\">";
624
            $html .= "<option>" . $selectLabel . "</option>";
625
            foreach ($this->bulkActions as $bulkAction) {
626
                $v = $bulkAction->getName();
627
                $xhr = $bulkAction->getXhr();
628
                $destructive = $bulkAction->getDestructive();
629
                $html .= "<option value=\"$v\" data-xhr=\"$xhr\" data-destructive=\"$destructive\">" . $bulkAction->getLabel() . "</option>";
630
            }
631
            $html .= "</select>";
632
            $html .= "<button class=\"tabulator-bulk-confirm btn\">" . $confirmLabel . "</button>";
633
        }
634
        $html .= '</div>';
635
        $html .= '</div>';
636
        return $html;
637
    }
638
639
    public function JsonOptions(): string
640
    {
641
        $this->processLinks();
642
643
        $data = $this->list ?? [];
644
        if ($this->autoloadDataList && $data instanceof DataList) {
645
            $data = null;
646
        }
647
        $opts = $this->options;
648
        $opts['columnDefaults'] = $this->columnDefaults;
649
650
        if (empty($this->columns)) {
651
            $opts['autoColumns'] = true;
652
        } else {
653
            $opts['columns'] = array_values($this->columns);
654
        }
655
656
        if ($data && is_iterable($data)) {
657
            if ($data instanceof ArrayList) {
658
                $data = $data->toArray();
659
            } else {
660
                if (is_iterable($data) && !is_array($data)) {
661
                    $data = iterator_to_array($data);
662
                }
663
            }
664
            $opts['data'] = $data;
665
        }
666
667
        // i18n
668
        $locale = strtolower(str_replace('_', '-', i18n::get_locale()));
669
        $paginationTranslations = [
670
            "first" => _t("TabulatorPagination.first", "First"),
671
            "first_title" =>  _t("TabulatorPagination.first_title", "First Page"),
672
            "last" =>  _t("TabulatorPagination.last", "Last"),
673
            "last_title" => _t("TabulatorPagination.last_title", "Last Page"),
674
            "prev" => _t("TabulatorPagination.prev", "Previous"),
675
            "prev_title" =>  _t("TabulatorPagination.prev_title", "Previous Page"),
676
            "next" => _t("TabulatorPagination.next", "Next"),
677
            "next_title" =>  _t("TabulatorPagination.next_title", "Next Page"),
678
            "all" =>  _t("TabulatorPagination.all", "All"),
679
        ];
680
        // This will always default to last icon if present
681
        $customIcons = self::config()->custom_pagination_icons;
682
        if (!empty($customIcons)) {
683
            $paginationTranslations['first'] = $customIcons['first'] ?? "<<";
684
            $paginationTranslations['last'] = $customIcons['last'] ?? ">>";
685
            $paginationTranslations['prev'] = $customIcons['prev'] ?? "<";
686
            $paginationTranslations['next'] = $customIcons['next'] ?? ">";
687
        }
688
        $dataTranslations = [
689
            "loading" => _t("TabulatorData.loading", "Loading"),
690
            "error" => _t("TabulatorData.error", "Error"),
691
        ];
692
        $groupsTranslations = [
693
            "item" => _t("TabulatorGroups.item", "Item"),
694
            "items" => _t("TabulatorGroups.items", "Items"),
695
        ];
696
        $headerFiltersTranslations = [
697
            "default" => _t("TabulatorHeaderFilters.default", "filter column..."),
698
        ];
699
        $bulkActionsTranslations = [
700
            "no_action" => _t("TabulatorBulkActions.no_action", "Please select an action"),
701
            "no_records" => _t("TabulatorBulkActions.no_records", "Please select a record"),
702
            "destructive" => _t("TabulatorBulkActions.destructive", "Confirm destructive action ?"),
703
        ];
704
        $translations = [
705
            'data' => $dataTranslations,
706
            'groups' => $groupsTranslations,
707
            'pagination' => $paginationTranslations,
708
            'headerFilters' => $headerFiltersTranslations,
709
            'bulkActions' => $bulkActionsTranslations,
710
        ];
711
        $opts['locale'] = $locale;
712
        $opts['langs'] = [
713
            $locale => $translations
714
        ];
715
716
        $format = Director::isDev() ? JSON_PRETTY_PRINT : 0;
717
        $json = json_encode($opts, $format);
718
719
        // Escape functions by namespace (see TabulatorField.js)
720
        foreach ($this->jsNamespaces as $ns) {
721
            $json = preg_replace('/"(' . $ns . '\.[a-zA-Z]*)"/', "$1", $json);
722
            // Keep static namespaces
723
            $json = str_replace("*" . $ns, $ns, $json);
724
        }
725
726
        return $json;
727
    }
728
729
    /**
730
     * @param Controller $controller
731
     * @return CompatLayerInterface
732
     */
733
    public function getCompatLayer(Controller $controller)
734
    {
735
        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...
736
            return new SilverstripeAdminCompat();
737
        }
738
        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...
739
            return new AdminiCompat();
740
        }
741
    }
742
743
    public function getAttributes()
744
    {
745
        $attrs = parent::getAttributes();
746
        unset($attrs['type']);
747
        unset($attrs['name']);
748
        return $attrs;
749
    }
750
751
    public function getOption(string $k)
752
    {
753
        return $this->options[$k] ?? null;
754
    }
755
756
    public function setOption(string $k, $v): self
757
    {
758
        $this->options[$k] = $v;
759
        return $this;
760
    }
761
762
    public function makeHeadersSticky(): self
763
    {
764
        $this->addExtraClass("tabulator-sticky");
765
        return $this;
766
    }
767
768
    public function setRemoteSource(string $url, array $extraParams = [], bool $dataResponse = false): self
769
    {
770
        $this->setOption("ajaxURL", $url); //set url for ajax request
771
        $params = array_merge([
772
            'SecurityID' => SecurityToken::getSecurityID()
773
        ], $extraParams);
774
        $this->setOption("ajaxParams", $params);
775
        // Accept response where data is nested under the data key
776
        if ($dataResponse) {
777
            $this->setOption("ajaxResponse", self::JS_DATA_AJAX_RESPONSE);
778
        }
779
        return $this;
780
    }
781
782
    /**
783
     * @link http://www.tabulator.info/docs/5.2/page#remote
784
     * @param string $url
785
     * @param array $params
786
     * @param integer $pageSize
787
     * @param integer $initialPage
788
     */
789
    public function setRemotePagination(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1): self
790
    {
791
        $this->setOption("pagination", true); //enable pagination
792
        $this->setOption("paginationMode", 'remote'); //enable remote pagination
793
        $this->setRemoteSource($url, $params);
794
        if (!$pageSize) {
795
            $pageSize = $this->pageSize;
796
        }
797
        $this->setOption("paginationSize", $pageSize);
798
        $this->setOption("paginationInitialPage", $initialPage);
799
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/5.2/page#counter
800
        return $this;
801
    }
802
803
    public function wizardRemotePagination(int $pageSize = 0, int $initialPage = 1, array $params = []): self
804
    {
805
        $this->setRemotePagination($this->TempLink('load', false), $params, $pageSize, $initialPage);
806
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/5.2/sort#ajax-sort
807
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/5.2/filter#ajax-filter
808
        return $this;
809
    }
810
811
    public function setProgressiveLoad(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0): self
812
    {
813
        $this->setOption("ajaxURL", $url);
814
        if (!empty($params)) {
815
            $this->setOption("ajaxParams", $params);
816
        }
817
        $this->setOption("progressiveLoad", $mode);
818
        if ($scrollMargin > 0) {
819
            $this->setOption("progressiveLoadScrollMargin", $scrollMargin);
820
        }
821
        if (!$pageSize) {
822
            $pageSize = $this->pageSize;
823
        }
824
        $this->setOption("paginationSize", $pageSize);
825
        $this->setOption("paginationInitialPage", $initialPage);
826
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/5.2/page#counter
827
        return $this;
828
    }
829
830
    public function wizardProgressiveLoad(int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0, array $extraParams = []): self
831
    {
832
        $params = array_merge([
833
            'SecurityID' => SecurityToken::getSecurityID()
834
        ], $extraParams);
835
        $this->setProgressiveLoad($this->TempLink('load', false), $params, $pageSize, $initialPage, $mode, $scrollMargin);
836
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/5.2/sort#ajax-sort
837
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/5.2/filter#ajax-filter
838
        return $this;
839
    }
840
841
    public function wizardResponsiveCollapse(bool $startOpen = false, string $mode = "collapse"): self
842
    {
843
        $this->setOption("responsiveLayout", $mode);
844
        $this->setOption("responsiveLayoutCollapseStartOpen", $startOpen);
845
        $this->columns = array_merge([
846
            'ui_responsive_collapse' => [
847
                "cssClass" => 'tabulator-cell-btn',
848
                'formatter' => 'responsiveCollapse',
849
                'headerSort' => false,
850
                'width' => 40,
851
            ]
852
        ], $this->columns);
853
        return $this;
854
    }
855
856
    public function wizardDataTree(bool $startExpanded = false, bool $filter = false, bool $sort = false, string $el = null): self
857
    {
858
        $this->setOption("dataTree", true);
859
        $this->setOption("dataTreeStartExpanded", $startExpanded);
860
        $this->setOption("dataTreeFilter", $filter);
861
        $this->setOption("dataTreeSort", $sort);
862
        if ($el) {
863
            $this->setOption("dataTreeElementColumn", $el);
864
        }
865
        return $this;
866
    }
867
868
    public function wizardSelectable(array $actions = []): self
869
    {
870
        $this->columns = array_merge([
871
            'ui_selectable' => [
872
                "hozAlign" => 'center',
873
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector',
874
                'formatter' => 'rowSelection',
875
                'titleFormatter' => 'rowSelection',
876
                'headerSort' => false,
877
                'width' => 40,
878
                'cellClick' => 'SSTabulator.forwardClick',
879
            ]
880
        ], $this->columns);
881
        $this->setBulkActions($actions);
882
        return $this;
883
    }
884
885
    public function wizardMoveable(string $callback = "SSTabulator.rowMoved"): self
886
    {
887
        $this->moveUrl = $this->TempLink("item/{ID}/ajaxMove", false);
888
        $this->setOption("movableRows", true);
889
        $this->addListener("rowMoved", $callback);
890
        $this->columns = array_merge([
891
            'ui_move' => [
892
                "hozAlign" => 'center',
893
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector',
894
                'rowHandle' => true,
895
                'formatter' => 'handle',
896
                'headerSort' => false,
897
                'frozen' => true,
898
                'width' => 40,
899
            ]
900
        ], $this->columns);
901
        return $this;
902
    }
903
904
    /**
905
     * @param string $field
906
     * @param string $toggleElement arrow|header|false (header by default)
907
     * @param boolean $isBool
908
     * @return void
909
     */
910
    public function wizardGroupBy(string $field, string $toggleElement = 'header', bool $isBool = false)
911
    {
912
        $this->setOption("groupBy", $field);
913
        $this->setOption("groupToggleElement", $toggleElement);
914
        if ($isBool) {
915
            $this->setOption("groupHeader", self::JS_BOOL_GROUP_HEADER);
916
        }
917
    }
918
919
    /**
920
     * @param HTTPRequest $request
921
     * @return HTTPResponse
922
     */
923
    public function handleItem($request)
924
    {
925
        // Our getController could either give us a true Controller, if this is the top-level GridField.
926
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
927
        $requestHandler = $this->getForm()->getController();
928
        $record = $this->getRecordFromRequest($request);
929
        if (!$record) {
930
            return $requestHandler->httpError(404, 'That record was not found');
931
        }
932
        $handler = $this->getItemRequestHandler($record, $requestHandler);
933
        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...
934
    }
935
936
    /**
937
     * @param HTTPRequest $request
938
     * @return HTTPResponse
939
     */
940
    public function handleTool($request)
941
    {
942
        // Our getController could either give us a true Controller, if this is the top-level GridField.
943
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
944
        $requestHandler = $this->getForm()->getController();
945
        $tool = $this->getToolFromRequest($request);
946
        if (!$tool) {
947
            return $requestHandler->httpError(404, 'That tool was not found');
948
        }
949
        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...
950
    }
951
952
    /**
953
     * @param HTTPRequest $request
954
     * @return HTTPResponse
955
     */
956
    public function handleBulkAction($request)
957
    {
958
        // Our getController could either give us a true Controller, if this is the top-level GridField.
959
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
960
        $requestHandler = $this->getForm()->getController();
961
        $bulkAction = $this->getBulkActionFromRequest($request);
962
        if (!$bulkAction) {
963
            return $requestHandler->httpError(404, 'That bulk action was not found');
964
        }
965
        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...
966
    }
967
968
    /**
969
     * @return string name of {@see TabulatorGrid_ItemRequest} subclass
970
     */
971
    public function getItemRequestClass(): string
972
    {
973
        if ($this->itemRequestClass) {
974
            return $this->itemRequestClass;
975
        } elseif (ClassInfo::exists(static::class . '_ItemRequest')) {
976
            return static::class . '_ItemRequest';
977
        }
978
        return TabulatorGrid_ItemRequest::class;
979
    }
980
981
    /**
982
     * Build a request handler for the given record
983
     *
984
     * @param DataObject $record
985
     * @param RequestHandler $requestHandler
986
     * @return TabulatorGrid_ItemRequest
987
     */
988
    protected function getItemRequestHandler($record, $requestHandler)
989
    {
990
        $class = $this->getItemRequestClass();
991
        $assignedClass = $this->itemRequestClass;
992
        $this->extend('updateItemRequestClass', $class, $record, $requestHandler, $assignedClass);
993
        /** @var TabulatorGrid_ItemRequest $handler */
994
        $handler = Injector::inst()->createWithArgs(
995
            $class,
996
            [$this, $record, $requestHandler]
997
        );
998
        if ($template = $this->getTemplate()) {
999
            $handler->setTemplate($template);
1000
        }
1001
        $this->extend('updateItemRequestHandler', $handler);
1002
        return $handler;
1003
    }
1004
1005
    public function getStateKey()
1006
    {
1007
        return $this->getName();
1008
    }
1009
1010
    public function getState(HTTPRequest $request)
1011
    {
1012
        $stateKey = $this->getName();
1013
        $state = $request->getSession()->get("TabulatorState[$stateKey]");
1014
        return $state ?? [
1015
            'page' => 1,
1016
            'limit' => $this->pageSize,
1017
            'sort' => [],
1018
            'filter' => [],
1019
        ];
1020
    }
1021
1022
    public function setState(HTTPRequest $request, $state)
1023
    {
1024
        $stateKey = $this->getName();
1025
        $request->getSession()->set("TabulatorState[$stateKey]", $state);
1026
    }
1027
1028
    /**
1029
     * Deprecated in favor of modular behaviour
1030
     * @deprecated
1031
     * @return string
1032
     */
1033
    public function getInitScript(): string
1034
    {
1035
        $JsonOptions = $this->JsonOptions();
1036
        $ID = $this->ID();
1037
        $script = "SSTabulator.init(\"#$ID\", $JsonOptions);";
1038
        return $script;
1039
    }
1040
1041
    /**
1042
     * Provides the configuration for this instance
1043
     *
1044
     * This is really useful in the context of the admin as it will be served over
1045
     * ajax
1046
     *
1047
     * Deprecated in favor of modular behaviour
1048
     * @deprecated
1049
     * @param HTTPRequest $request
1050
     * @return HTTPResponse
1051
     */
1052
    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

1052
    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...
1053
    {
1054
        if (!$this->useConfigProvider) {
1055
            return $this->httpError(404);
1056
        }
1057
        $response = new HTTPResponse($this->getInitScript());
0 ignored issues
show
Deprecated Code introduced by
The function LeKoala\Tabulator\TabulatorGrid::getInitScript() has been deprecated. ( Ignorable by Annotation )

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

1057
        $response = new HTTPResponse(/** @scrutinizer ignore-deprecated */ $this->getInitScript());
Loading history...
1058
        $response->addHeader('Content-Type', 'application/script');
1059
        return $response;
1060
    }
1061
1062
    /**
1063
     * Provides autocomplete lists
1064
     *
1065
     * @param HTTPRequest $request
1066
     * @return HTTPResponse
1067
     */
1068
    public function autocomplete(HTTPRequest $request)
1069
    {
1070
        if ($this->isDisabled() || $this->isReadonly()) {
1071
            return $this->httpError(403);
1072
        }
1073
        $SecurityID = $request->getVar('SecurityID');
1074
        if (!SecurityToken::inst()->check($SecurityID)) {
1075
            return $this->httpError(404, "Invalid SecurityID");
1076
        }
1077
1078
        $name = $request->getVar("Column");
1079
        $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

1079
        $col = $this->getColumn(/** @scrutinizer ignore-type */ $name);
Loading history...
1080
        if (!$col) {
1081
            return $this->httpError(403, "Invalid column");
1082
        }
1083
1084
        $term = '%' . $request->getVar('term') . '%';
1085
1086
        $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

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

1295
            throw new RuntimeException("Value is not a ArrayList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1296
        }
1297
        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...
1298
    }
1299
1300
    public function hasDataList(): bool
1301
    {
1302
        return $this->list instanceof DataList;
1303
    }
1304
1305
    public function getDataList(): DataList
1306
    {
1307
        if (!$this->list instanceof DataList) {
1308
            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

1308
            throw new RuntimeException("Value is not a DataList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1309
        }
1310
        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...
1311
    }
1312
1313
    public function getManipulatedData(int $limit, int $offset, array $sort = null, array $filter = null): array
1314
    {
1315
        if (!$this->hasDataList()) {
1316
            $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

1316
            /** @scrutinizer ignore-call */ 
1317
            $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...
1317
1318
            $lastRow = $this->list->count();
1319
            $lastPage = ceil($lastRow / $limit);
1320
1321
            $result = [
1322
                'last_row' => $lastRow,
1323
                'last_page' => $lastPage,
1324
                'data' => $data,
1325
            ];
1326
1327
            return $result;
1328
        }
1329
1330
        $dataList = $this->getDataList();
1331
1332
        $schema = DataObject::getSchema();
1333
        $dataClass = $dataList->dataClass();
1334
        /** @var DataObject $singleton */
1335
        $singleton = singleton($dataClass);
1336
        $resolutionMap = [];
1337
1338
        $sortSql = [];
1339
        if ($sort) {
1340
            foreach ($sort as $sortValues) {
1341
                $cols = array_keys($this->columns);
1342
                $field = $sortValues['field'];
1343
                if (!in_array($field, $cols)) {
1344
                    throw new Exception("Invalid sort field: $field");
1345
                }
1346
                $dir = $sortValues['dir'];
1347
                if (!in_array($dir, ['asc', 'desc'])) {
1348
                    throw new Exception("Invalid sort dir: $dir");
1349
                }
1350
1351
                // Nested sort
1352
                if (strpos($field, '.') !== false) {
1353
                    $parts = explode(".", $field);
1354
                    if (!isset($resolutionMap[$parts[0]])) {
1355
                        $resolutionMap[$parts[0]] = singleton($dataClass)->relObject($parts[0]);
1356
                    }
1357
                    $relatedObject = get_class($resolutionMap[$parts[0]]);
1358
                    $tableName = $schema->tableForField($relatedObject, $parts[1]);
1359
                    $baseIDColumn = $schema->sqlColumnForField($dataClass, 'ID');
1360
                    $tableAlias = $parts[0];
1361
                    $dataList = $dataList->leftJoin($tableName, "\"{$tableAlias}\".\"ID\" = {$baseIDColumn}", $tableAlias);
1362
                }
1363
1364
                $sortSql[] = $field . ' ' . $dir;
1365
            }
1366
        }
1367
        if (!empty($sortSql)) {
1368
            $dataList = $dataList->sort(implode(", ", $sortSql));
1369
        }
1370
1371
        // Filtering is an array of field/type/value arrays
1372
        $where = [];
1373
        if ($filter) {
1374
            foreach ($filter as $filterValues) {
1375
                $cols = array_keys($this->columns);
1376
                $field = $filterValues['field'];
1377
                if (!in_array($field, $cols)) {
1378
                    throw new Exception("Invalid sort field: $field");
1379
                }
1380
                $value = $filterValues['value'];
1381
                $type = $filterValues['type'];
1382
1383
                // Strict value
1384
                if ($value === "true") {
1385
                    $value = true;
1386
                } elseif ($value === "false") {
1387
                    $value = false;
1388
                }
1389
1390
                switch ($type) {
1391
                    case "=":
1392
                        $where["$field"] = $value;
1393
                        break;
1394
                    case "!=":
1395
                        $where["$field:not"] = $value;
1396
                        break;
1397
                    case "like":
1398
                        $where["$field:PartialMatch:nocase"] = $value;
1399
                        break;
1400
                    case "keywords":
1401
                        $where["$field:PartialMatch:nocase"] = str_replace(" ", "%", $value);
1402
                        break;
1403
                    case "starts":
1404
                        $where["$field:StartsWith:nocase"] = $value;
1405
                        break;
1406
                    case "ends":
1407
                        $where["$field:EndsWith:nocase"] = $value;
1408
                        break;
1409
                    case "<":
1410
                        $where["$field:LessThan:nocase"] = $value;
1411
                        break;
1412
                    case "<=":
1413
                        $where["$field:LessThanOrEqual:nocase"] = $value;
1414
                        break;
1415
                    case ">":
1416
                        $where["$field:GreaterThan:nocase"] = $value;
1417
                        break;
1418
                    case ">=":
1419
                        $where["$field:GreaterThanOrEqual:nocase"] = $value;
1420
                        break;
1421
                    case "in":
1422
                        $where["$field"] = $value;
1423
                        break;
1424
                    case "regex":
1425
                        $dataList = $dataList->where('REGEXP ' . Convert::raw2sql($value));
1426
                        break;
1427
                    default:
1428
                        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...
1429
                }
1430
            }
1431
        }
1432
        if (!empty($where)) {
1433
            $dataList = $dataList->filter($where);
1434
        }
1435
1436
        $lastRow = $dataList->count();
1437
        $lastPage = ceil($lastRow / $limit);
1438
1439
        $data = [];
1440
        /** @var DataObject $record */
1441
        foreach ($dataList->limit($limit, $offset) as $record) {
1442
            if ($record->hasMethod('canView') && !$record->canView()) {
1443
                continue;
1444
            }
1445
1446
            $item = [
1447
                'ID' => $record->ID,
1448
            ];
1449
            $nested = [];
1450
            foreach ($this->columns as $col) {
1451
                $field = $col['field'] ?? null; // actions don't have field
1452
                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

1452
                if (strpos(/** @scrutinizer ignore-type */ $field, '.') !== false) {
Loading history...
1453
                    $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

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