Passed
Push — master ( e55602...db7ee9 )
by Thomas
11:40
created

TabulatorGrid::updateColumn()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 7
rs 10
cc 2
nc 2
nop 2
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.4?#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.4/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.4/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.4';
118
119
    /**
120
     * @config
121
     */
122
    private static string $luxon_version = '3';
123
124
    /**
125
     * @config
126
     */
127
    private static string $last_icon_version = '2';
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
     * @config
156
     */
157
    private static bool $enable_js_modules = true;
158
159
    /**
160
     * @link http://www.tabulator.info/docs/5.4/options
161
     * @config
162
     */
163
    private static array $default_options = [
164
        'index' => "ID", // http://tabulator.info/docs/5.4/data#row-index
165
        'layout' => 'fitColumns', // http://www.tabulator.info/docs/5.4/layout#layout
166
        'height' => '100%', // http://www.tabulator.info/docs/5.4/layout#height-fixed
167
        'responsiveLayout' => "hide", // http://www.tabulator.info/docs/5.4/layout#responsive
168
        'rowFormatter' => "SSTabulator.simpleRowFormatter", // http://tabulator.info/docs/5.4/format#row
169
    ];
170
171
    /**
172
     * @link http://tabulator.info/docs/5.4/columns#defaults
173
     * @config
174
     */
175
    private static array $default_column_options = [
176
        'resizable' => false,
177
        'tooltip' => 'SSTabulator.expandTooltip',
178
    ];
179
180
    private static bool $enable_ajax_init = true;
181
182
    /**
183
     * @config
184
     */
185
    private static array $custom_pagination_icons = [];
186
187
    /**
188
     * @config
189
     */
190
    private static bool $default_lazy_init = false;
191
192
    /**
193
     * Data source.
194
     */
195
    protected ?SS_List $list;
196
197
    /**
198
     * @link http://www.tabulator.info/docs/5.4/columns
199
     */
200
    protected array $columns = [];
201
202
    /**
203
     * @link http://tabulator.info/docs/5.4/columns#defaults
204
     */
205
    protected array $columnDefaults = [];
206
207
    /**
208
     * @link http://www.tabulator.info/docs/5.4/options
209
     */
210
    protected array $options = [];
211
212
    protected bool $autoloadDataList = true;
213
214
    protected bool $rowClickTriggersAction = false;
215
216
    protected int $pageSize = 10;
217
218
    protected string $itemRequestClass = '';
219
220
    protected string $modelClass = '';
221
222
    protected bool $lazyInit = false;
223
224
    protected array $tools = [];
225
226
    /**
227
     * @var AbstractBulkAction[]
228
     */
229
    protected array $bulkActions = [];
230
231
    protected array $listeners = [];
232
233
    protected array $jsNamespaces = [
234
        'SSTabulator'
235
    ];
236
237
    protected array $linksOptions = [
238
        'ajaxURL'
239
    ];
240
241
    protected array $dataAttributes = [];
242
243
    protected string $controllerFunction = "";
244
245
    protected bool $useConfigProvider = false;
246
247
    protected bool $useInitScript = false;
248
249
    protected string $editUrl = "";
250
251
    protected string $moveUrl = "";
252
253
    protected string $bulkUrl = "";
254
255
    /**
256
     * @param string $fieldName
257
     * @param string|null|bool $title
258
     * @param SS_List $value
259
     */
260
    public function __construct($name, $title = null, $value = null)
261
    {
262
        parent::__construct($name, $title, $value);
263
        $this->options = self::config()->default_options ?? [];
264
        $this->columnDefaults = self::config()->default_column_options ?? [];
265
        $this->setLazyInit(self::config()->default_lazy_init);
266
267
        // We don't want regular setValue for this since it would break with loadFrom logic
268
        if ($value) {
269
            $this->setList($value);
270
        }
271
    }
272
273
    /**
274
     * This helps if some third party code expects the TabulatorGrid to be a GridField
275
     * Only works to a really basic extent
276
     */
277
    public function getConfig(): GridFieldConfig
278
    {
279
        return new GridFieldConfig;
280
    }
281
282
    /**
283
     * This helps if some third party code expects the TabulatorGrid to be a GridField
284
     * Only works to a really basic extent
285
     */
286
    public function setConfig($config)
287
    {
288
        // ignore
289
    }
290
291
    /**
292
     * Temporary link that will be replaced by a real link by processLinks
293
     *
294
     * @param string $action
295
     * @return string
296
     */
297
    public function TempLink(string $action, bool $controller = true): string
298
    {
299
        // It's an absolute link
300
        if (strpos($action, '/') === 0 || strpos($action, 'http') === 0) {
301
            return $action;
302
        }
303
        // Already temp
304
        if (strpos($action, ':') !== false) {
305
            return $action;
306
        }
307
        $prefix = $controller ? "controller" : "form";
308
        return "$prefix:$action";
309
    }
310
311
    public function ControllerLink(string $action): string
312
    {
313
        return $this->getForm()->getController()->Link($action);
314
    }
315
316
    public function getCreateLink(): string
317
    {
318
        return Controller::join_links($this->Link('item'), 'new');
319
    }
320
321
    /**
322
     * @param FieldList $fields
323
     * @param string $name
324
     * @return TabulatorGrid|null
325
     */
326
    public static function replaceGridField(FieldList $fields, string $name)
327
    {
328
        /** @var \SilverStripe\Forms\GridField\GridField $gridField */
329
        $gridField = $fields->dataFieldByName($name);
330
        if (!$gridField) {
0 ignored issues
show
introduced by
$gridField is of type SilverStripe\Forms\GridField\GridField, thus it always evaluated to true.
Loading history...
331
            return;
332
        }
333
        if ($gridField instanceof TabulatorGrid) {
0 ignored issues
show
introduced by
$gridField is never a sub-type of LeKoala\Tabulator\TabulatorGrid.
Loading history...
334
            return $gridField;
335
        }
336
        $tabulatorGrid = new TabulatorGrid($name, $gridField->Title(), $gridField->getList());
337
        // In the cms, this is mostly never happening
338
        if ($gridField->getForm()) {
339
            $tabulatorGrid->setForm($gridField->getForm());
340
        }
341
        $tabulatorGrid->configureFromDataObject($gridField->getModelClass());
342
        $tabulatorGrid->setLazyInit(true);
343
        $fields->replaceField($name, $tabulatorGrid);
344
345
        return $tabulatorGrid;
346
    }
347
348
    public function configureFromDataObject($className = null, bool $clear = true): void
349
    {
350
        $this->columns = [];
351
352
        if (!$className) {
353
            $className = $this->getModelClass();
354
        }
355
        if (!$className) {
356
            throw new RuntimeException("Could not find the model class");
357
        }
358
        $this->modelClass = $className;
359
360
        /** @var DataObject $singl */
361
        $singl = singleton($className);
362
363
        // Mock some base columns using SilverStripe built-in methods
364
        $columns = [];
365
366
        foreach ($singl->summaryFields() as $field => $title) {
367
            // Deal with this in load() instead
368
            // if (strpos($field, '.') !== false) {
369
            // $fieldParts = explode(".", $field);
370
371
            // It can be a relation Users.Count or a field Field.Nice
372
            // $classOrField = $fieldParts[0];
373
            // $relationOrMethod = $fieldParts[1];
374
            // }
375
            $title = str_replace(".", " ", $title);
376
            $columns[$field] = [
377
                'field' => $field,
378
                'title' => $title,
379
            ];
380
381
            $dbObject = $singl->dbObject($field);
382
            if ($dbObject) {
383
                if ($dbObject instanceof DBBoolean) {
384
                    $columns[$field]['formatter'] = "SSTabulator.customTickCrossFormatter";
385
                }
386
            }
387
        }
388
        foreach ($singl->searchableFields() as $key => $searchOptions) {
389
            /*
390
            "filter" => "NameOfTheFilter"
391
            "field" => "SilverStripe\Forms\FormField"
392
            "title" => "Title of the field"
393
            */
394
            if (!isset($columns[$key])) {
395
                continue;
396
            }
397
            $columns[$key]['headerFilter'] = true;
398
            // $columns[$key]['headerFilterPlaceholder'] = $searchOptions['title'];
399
            //TODO: implement filter mapping
400
            switch ($searchOptions['filter']) {
401
                default:
402
                    $columns[$key]['headerFilterFunc'] =  "like";
403
                    break;
404
            }
405
406
            // Restrict based on data type
407
            $dbObject = $singl->dbObject($key);
408
            if ($dbObject) {
409
                if ($dbObject instanceof DBBoolean) {
410
                    $columns[$key]['headerFilter'] = 'tickCross';
411
                    $columns[$key]['headerFilterFunc'] =  "=";
412
                    $columns[$key]['headerFilterParams'] =  [
413
                        'tristate' => true
414
                    ];
415
                }
416
                if ($dbObject instanceof DBEnum) {
417
                    $columns[$key]['headerFilter'] = 'list';
418
                    $columns[$key]['headerFilterFunc'] =  "=";
419
                    $columns[$key]['headerFilterParams'] =  [
420
                        'values' => $dbObject->enumValues()
421
                    ];
422
                }
423
            }
424
        }
425
426
        // Allow customizing our columns based on record
427
        if ($singl->hasMethod('tabulatorColumns')) {
428
            $fields = $singl->tabulatorColumns();
429
            if (!is_array($fields)) {
430
                throw new RuntimeException("tabulatorColumns must return an array");
431
            }
432
            foreach ($fields as $key => $columnOptions) {
433
                $baseOptions = $columns[$key] ?? [];
434
                $columns[$key] = array_merge($baseOptions, $columnOptions);
435
            }
436
        }
437
438
        $this->extend('updateConfiguredColumns', $columns);
439
440
        foreach ($columns as $col) {
441
            $this->addColumn($col['field'], $col['title'], $col);
442
        }
443
444
        // Sortable ?
445
        if ($singl->hasField('Sort')) {
446
            $this->wizardMoveable();
447
        }
448
449
        // Actions
450
        // We use a pseudo link, because maybe we cannot call Link() yet if it's not linked to a form
451
452
        $this->bulkUrl = $this->TempLink("bulkAction/", false);
453
454
        // - Core actions, handled by TabulatorGrid
455
        $itemUrl = $this->TempLink('item/{ID}', false);
456
        if ($singl->canEdit()) {
457
            $this->addButton("ui_edit", $itemUrl, "edit", "Edit");
458
            $this->editUrl = $this->TempLink("item/{ID}/ajaxEdit", false);
459
        } elseif ($singl->canView()) {
460
            $this->addButton("ui_view", $itemUrl, "visibility", "View");
461
        }
462
463
        // - Tools
464
        $this->tools = [];
465
        if ($singl->canCreate()) {
466
            $this->addTool(self::POS_START, new TabulatorAddNewButton($this), 'add_new');
0 ignored issues
show
Unused Code introduced by
The call to LeKoala\Tabulator\Tabula...ewButton::__construct() has too many arguments starting with $this. ( Ignorable by Annotation )

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

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

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

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

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

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

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

Loading history...
470
        }
471
472
        // - Custom actions are forwarded to the model itself
473
        if ($singl->hasMethod('tabulatorRowActions')) {
474
            $rowActions = $singl->tabulatorRowActions();
475
            if (!is_array($rowActions)) {
476
                throw new RuntimeException("tabulatorRowActions must return an array");
477
            }
478
            foreach ($rowActions as $key => $actionConfig) {
479
                $action = $actionConfig['action'] ?? $key;
480
                $url = $this->TempLink("item/{ID}/customAction/$action", false);
481
                $icon = $actionConfig['icon'] ?? "cog";
482
                $title = $actionConfig['title'] ?? "";
483
484
                $button = $this->makeButton($url, $icon, $title);
485
                if (!empty($actionConfig['ajax'])) {
486
                    $button['formatterParams']['ajax'] = true;
487
                }
488
                $this->addButtonFromArray("ui_customaction_$action", $button);
489
            }
490
        }
491
492
        $this->setRowClickTriggersAction(true);
493
    }
494
495
    public static function requirements(): void
496
    {
497
        $use_cdn = self::config()->use_cdn;
498
        $use_custom_build = self::config()->use_custom_build;
499
        $theme = self::config()->theme; // simple, midnight, modern or framework
500
        $version = self::config()->version;
501
        $luxon_version = self::config()->luxon_version;
502
        $enable_luxon = self::config()->enable_luxon;
503
        $last_icon_version = self::config()->last_icon_version;
504
        $enable_last_icon = self::config()->enable_last_icon;
505
        $enable_js_modules = self::config()->enable_js_modules;
506
507
        $jsOpts = [];
508
        if ($enable_js_modules) {
509
            $jsOpts['type'] = 'module';
510
        }
511
512
        if ($use_cdn) {
513
            $baseDir = "https://cdn.jsdelivr.net/npm/tabulator-tables@$version/dist";
514
        } else {
515
            $asset = ModuleResourceLoader::resourceURL('lekoala/silverstripe-tabulator:client/cdn/js/tabulator.min.js');
516
            $baseDir = dirname(dirname($asset));
517
        }
518
519
        if ($luxon_version && $enable_luxon) {
520
            // Do not load as module or we would get undefined luxon global var
521
            Requirements::javascript("https://cdn.jsdelivr.net/npm/luxon@$luxon_version/build/global/luxon.min.js");
522
        }
523
        if ($last_icon_version && $enable_last_icon) {
524
            Requirements::css("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.css");
525
            // Do not load as module even if asked to ensure load speed
526
            Requirements::javascript("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.js");
527
        }
528
        if ($use_custom_build) {
529
            // if (Director::isDev() && !Director::is_ajax()) {
530
            //     Requirements::javascript("lekoala/silverstripe-tabulator:client/custom-tabulator.js");
531
            // } else {
532
            Requirements::javascript("lekoala/silverstripe-tabulator:client/custom-tabulator.min.js", $jsOpts);
533
            // }
534
            Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js', $jsOpts);
535
        } else {
536
            Requirements::javascript("$baseDir/js/tabulator.min.js", $jsOpts);
537
            Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js', $jsOpts);
538
        }
539
540
        if ($theme) {
541
            Requirements::css("$baseDir/css/tabulator_$theme.min.css");
542
        } else {
543
            Requirements::css("$baseDir/css/tabulator.min.css");
544
        }
545
546
        if ($theme && $theme == "bootstrap5") {
547
            Requirements::css('lekoala/silverstripe-tabulator:client/custom-tabulator.min.css');
548
        }
549
    }
550
551
    public function setValue($value, $data = null)
552
    {
553
        if ($value instanceof DataList) {
554
            $this->configureFromDataObject($value->dataClass());
555
        }
556
        return parent::setValue($value, $data);
557
    }
558
559
    public function getModularName()
560
    {
561
        return 'SSTabulator.createTabulator';
562
    }
563
564
    public function getModularSelector()
565
    {
566
        return '.tabulatorgrid';
567
    }
568
569
    public function getModularConfigName()
570
    {
571
        return str_replace('-', '_', $this->ID()) . '_config';
572
    }
573
574
    public function getModularConfig()
575
    {
576
        $JsonOptions = $this->JsonOptions();
577
        $configName = $this->getModularConfigName();
578
        $script = "var $configName = $JsonOptions";
579
        return $script;
580
    }
581
582
    public function Field($properties = [])
583
    {
584
        $this->addExtraClass(self::config()->theme);
585
        if ($this->lazyInit) {
586
            $this->setModularLazy($this->lazyInit);
587
        }
588
        if (self::config()->enable_requirements) {
589
            self::requirements();
590
        }
591
592
        // Make sure we can use a standalone version of the field without a form
593
        // Function should match the name
594
        if (!$this->form) {
595
            $this->form = new Form(Controller::curr(), $this->getControllerFunction());
596
        }
597
598
        // Data attributes for our custom behaviour
599
        $this->setDataAttribute("row-click-triggers-action", $this->rowClickTriggersAction);
600
        $customIcons = self::config()->custom_pagination_icons;
601
        $this->setDataAttribute("use-custom-pagination-icons", empty($customIcons));
602
603
        $this->setDataAttribute("listeners", $this->listeners);
604
        if ($this->editUrl) {
605
            $url = $this->processLink($this->editUrl);
606
            $this->setDataAttribute("edit-url", $url);
607
        }
608
        if ($this->moveUrl) {
609
            $url = $this->processLink($this->moveUrl);
610
            $this->setDataAttribute("move-url", $url);
611
        }
612
        if (!empty($this->bulkActions)) {
613
            $url = $this->processLink($this->bulkUrl);
614
            $this->setDataAttribute("bulk-url", $url);
615
        }
616
617
        if ($this->useConfigProvider) {
618
            $configLink = "/" . ltrim($this->Link("configProvider"), "/");
619
            $configLink .= "?t=" . time();
620
            // This cannot be loaded as a js module
621
            Requirements::javascript($configLink, ['type' => 'application/javascript', 'defer' => 'true']);
622
        } elseif ($this->useInitScript) {
623
            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

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

1101
        $response = new HTTPResponse(/** @scrutinizer ignore-deprecated */ $this->getInitScript());
Loading history...
1102
        $response->addHeader('Content-Type', 'application/script');
1103
        return $response;
1104
    }
1105
1106
    /**
1107
     * Provides autocomplete lists
1108
     *
1109
     * @param HTTPRequest $request
1110
     * @return HTTPResponse
1111
     */
1112
    public function autocomplete(HTTPRequest $request)
1113
    {
1114
        if ($this->isDisabled() || $this->isReadonly()) {
1115
            return $this->httpError(403);
1116
        }
1117
        $SecurityID = $request->getVar('SecurityID');
1118
        if (!SecurityToken::inst()->check($SecurityID)) {
1119
            return $this->httpError(404, "Invalid SecurityID");
1120
        }
1121
1122
        $name = $request->getVar("Column");
1123
        $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

1123
        $col = $this->getColumn(/** @scrutinizer ignore-type */ $name);
Loading history...
1124
        if (!$col) {
1125
            return $this->httpError(403, "Invalid column");
1126
        }
1127
1128
        // Don't use % term as it prevents use of indexes
1129
        $term = $request->getVar('term') . '%';
1130
        $term = str_replace(' ', '%', $term);
1131
1132
        $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

1132
        $parts = explode(".", /** @scrutinizer ignore-type */ $name);
Loading history...
1133
        if (count($parts) > 2) {
1134
            array_pop($parts);
1135
        }
1136
        if (count($parts) == 2) {
1137
            $class = $parts[0];
1138
            $field = $parts[1];
1139
        } elseif (count($parts) == 1) {
1140
            $class = preg_replace("/ID$/", "", $parts[0]);
1141
            $field = 'Title';
1142
        } else {
1143
            return $this->httpError(403, "Invalid field");
1144
        }
1145
1146
        /** @var DataObject $sng */
1147
        $sng = $class::singleton();
1148
        $baseTable = $sng->baseTable();
1149
1150
        $searchField = null;
1151
        $searchCandidates = [
1152
            $field, 'Name', 'Surname', 'Email', 'ID'
1153
        ];
1154
1155
        // Ensure field exists, this is really rudimentary
1156
        $db = $class::config()->db;
1157
        foreach ($searchCandidates as $searchCandidate) {
1158
            if ($searchField) {
1159
                continue;
1160
            }
1161
            if (isset($db[$searchCandidate])) {
1162
                $searchField = $searchCandidate;
1163
            }
1164
        }
1165
        $searchCols = [$searchField];
1166
1167
        // For members, do something better
1168
        if ($baseTable == 'Member') {
1169
            $searchField = ['FirstName', 'Surname'];
1170
            $searchCols = ['FirstName', 'Surname', 'Email'];
1171
        }
1172
1173
        if (!empty($col['editorParams']['customSearchField'])) {
1174
            $searchField = $col['editorParams']['customSearchField'];
1175
        }
1176
        if (!empty($col['editorParams']['customSearchCols'])) {
1177
            $searchCols = $col['editorParams']['customSearchCols'];
1178
        }
1179
1180
1181
        /** @var DataList $list */
1182
        $list = $sng::get();
1183
1184
        // Make sure at least one field is not null...
1185
        $where = [];
1186
        foreach ($searchCols as $searchCol) {
1187
            $where[] = $searchCol . ' IS NOT NULL';
1188
        }
1189
        $list = $list->where($where);
1190
        // ... and matches search term ...
1191
        $where = [];
1192
        foreach ($searchCols as $searchCol) {
1193
            $where[$searchCol . ' LIKE ?'] = $term;
1194
        }
1195
        $list = $list->whereAny($where);
1196
1197
        // ... and any user set requirements
1198
        if (!empty($col['editorParams']['where'])) {
1199
            // Deal with in clause
1200
            $customWhere = [];
1201
            foreach ($col['editorParams']['where'] as $col => $param) {
1202
                // For array, we need a IN statement with a ? for each value
1203
                if (is_array($param)) {
1204
                    $prepValue = [];
1205
                    $params = [];
1206
                    foreach ($param as $paramValue) {
1207
                        $params[] = $paramValue;
1208
                        $prepValue[] = "?";
1209
                    }
1210
                    $customWhere["$col IN (" . implode(',', $prepValue) . ")"] = $params;
1211
                } else {
1212
                    $customWhere["$col = ?"] = $param;
1213
                }
1214
            }
1215
            $list = $list->where($customWhere);
1216
        }
1217
1218
        $results = iterator_to_array($list);
1219
        $data = [];
1220
        foreach ($results as $record) {
1221
            if (is_array($searchField)) {
1222
                $labelParts = [];
1223
                foreach ($searchField as $sf) {
1224
                    $labelParts[] = $record->$sf;
1225
                }
1226
                $label = implode(" ", $labelParts);
1227
            } else {
1228
                $label = $record->$searchField;
1229
            }
1230
            $data[] = [
1231
                'value' => $record->ID,
1232
                'label' => $label,
1233
            ];
1234
        }
1235
1236
        $json = json_encode($data);
1237
        $response = new HTTPResponse($json);
1238
        $response->addHeader('Content-Type', 'application/script');
1239
        return $response;
1240
    }
1241
1242
    /**
1243
     * @link http://www.tabulator.info/docs/5.4/page#remote-response
1244
     * @param HTTPRequest $request
1245
     * @return HTTPResponse
1246
     */
1247
    public function load(HTTPRequest $request)
1248
    {
1249
        if ($this->isDisabled() || $this->isReadonly()) {
1250
            return $this->httpError(403);
1251
        }
1252
        $SecurityID = $request->getVar('SecurityID');
1253
        if (!SecurityToken::inst()->check($SecurityID)) {
1254
            return $this->httpError(404, "Invalid SecurityID");
1255
        }
1256
1257
        $page = (int) $request->getVar('page');
1258
        $limit = (int) $request->getVar('size');
1259
1260
        $sort = $request->getVar('sort');
1261
        $filter = $request->getVar('filter');
1262
1263
        // Persist state to allow the ItemEditForm to display navigation
1264
        $state = [
1265
            'page' => $page,
1266
            'limit' => $limit,
1267
            'sort' => $sort,
1268
            'filter' => $filter,
1269
        ];
1270
        $this->setState($request, $state);
1271
1272
        $offset = ($page - 1) * $limit;
1273
        $data = $this->getManipulatedData($limit, $offset, $sort, $filter);
1274
1275
        $response = new HTTPResponse(json_encode($data));
1276
        $response->addHeader('Content-Type', 'application/json');
1277
        return $response;
1278
    }
1279
1280
    /**
1281
     * @param HTTPRequest $request
1282
     * @return DataObject|null
1283
     */
1284
    protected function getRecordFromRequest(HTTPRequest $request): ?DataObject
1285
    {
1286
        /** @var DataObject $record */
1287
        if (is_numeric($request->param('ID'))) {
1288
            /** @var Filterable $dataList */
1289
            $dataList = $this->getList();
1290
            $record = $dataList->byID($request->param('ID'));
1291
        } else {
1292
            $record = Injector::inst()->create($this->getModelClass());
1293
        }
1294
        return $record;
1295
    }
1296
1297
    /**
1298
     * @param HTTPRequest $request
1299
     * @return AbstractTabulatorTool|null
1300
     */
1301
    protected function getToolFromRequest(HTTPRequest $request): ?AbstractTabulatorTool
1302
    {
1303
        $toolID = $request->param('ID');
1304
        $tool = $this->getTool($toolID);
1305
        return $tool;
1306
    }
1307
1308
    /**
1309
     * @param HTTPRequest $request
1310
     * @return AbstractBulkAction|null
1311
     */
1312
    protected function getBulkActionFromRequest(HTTPRequest $request): ?AbstractBulkAction
1313
    {
1314
        $toolID = $request->param('ID');
1315
        $tool = $this->getBulkAction($toolID);
1316
        return $tool;
1317
    }
1318
1319
    /**
1320
     * Get the value of a named field  on the given record.
1321
     *
1322
     * Use of this method ensures that any special rules around the data for this gridfield are
1323
     * followed.
1324
     *
1325
     * @param DataObject $record
1326
     * @param string $fieldName
1327
     *
1328
     * @return mixed
1329
     */
1330
    public function getDataFieldValue($record, $fieldName)
1331
    {
1332
        if ($record->hasMethod('relField')) {
1333
            return $record->relField($fieldName);
1334
        }
1335
1336
        if ($record->hasMethod($fieldName)) {
1337
            return $record->$fieldName();
1338
        }
1339
1340
        return $record->$fieldName;
1341
    }
1342
1343
    public function getManipulatedList(): SS_List
1344
    {
1345
        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...
1346
    }
1347
1348
    public function getList(): SS_List
1349
    {
1350
        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...
1351
    }
1352
1353
    public function setList(SS_List $list): self
1354
    {
1355
        if ($this->autoloadDataList && $list instanceof DataList) {
1356
            $this->wizardRemotePagination();
1357
        }
1358
        $this->list = $list;
1359
        return $this;
1360
    }
1361
1362
    public function hasArrayList(): bool
1363
    {
1364
        return $this->list instanceof ArrayList;
1365
    }
1366
1367
    public function getArrayList(): ArrayList
1368
    {
1369
        if (!$this->list instanceof ArrayList) {
1370
            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

1370
            throw new RuntimeException("Value is not a ArrayList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1371
        }
1372
        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...
1373
    }
1374
1375
    public function hasDataList(): bool
1376
    {
1377
        return $this->list instanceof DataList;
1378
    }
1379
1380
    /**
1381
     * A properly typed on which you can call byID
1382
     * @return ArrayList|DataList
1383
     */
1384
    public function getByIDList()
1385
    {
1386
        return $this->list;
1387
    }
1388
1389
    public function hasByIDList(): bool
1390
    {
1391
        return $this->hasDataList() || $this->hasArrayList();
1392
    }
1393
1394
    public function getDataList(): DataList
1395
    {
1396
        if (!$this->list instanceof DataList) {
1397
            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

1397
            throw new RuntimeException("Value is not a DataList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1398
        }
1399
        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...
1400
    }
1401
1402
    public function getManipulatedData(int $limit, int $offset, array $sort = null, array $filter = null): array
1403
    {
1404
        if (!$this->hasDataList()) {
1405
            $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

1405
            /** @scrutinizer ignore-call */ 
1406
            $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...
1406
1407
            $lastRow = $this->list->count();
1408
            $lastPage = ceil($lastRow / $limit);
1409
1410
            $result = [
1411
                'last_row' => $lastRow,
1412
                'last_page' => $lastPage,
1413
                'data' => $data,
1414
            ];
1415
1416
            return $result;
1417
        }
1418
1419
        $dataList = $this->getDataList();
1420
1421
        $schema = DataObject::getSchema();
1422
        $dataClass = $dataList->dataClass();
1423
        /** @var DataObject $singleton */
1424
        $singleton = singleton($dataClass);
1425
        $resolutionMap = [];
1426
1427
        $sortSql = [];
1428
        if ($sort) {
1429
            foreach ($sort as $sortValues) {
1430
                $cols = array_keys($this->columns);
1431
                $field = $sortValues['field'];
1432
                if (!in_array($field, $cols)) {
1433
                    throw new Exception("Invalid sort field: $field");
1434
                }
1435
                $dir = $sortValues['dir'];
1436
                if (!in_array($dir, ['asc', 'desc'])) {
1437
                    throw new Exception("Invalid sort dir: $dir");
1438
                }
1439
1440
                // Nested sort
1441
                if (strpos($field, '.') !== false) {
1442
                    $parts = explode(".", $field);
1443
1444
                    // Resolve relation only once in case of multiples similar keys
1445
                    if (!isset($resolutionMap[$parts[0]])) {
1446
                        $resolutionMap[$parts[0]] = $singleton->relObject($parts[0]);
1447
                    }
1448
                    // Not matching anything (maybe a formatting .Nice ?)
1449
                    if (!$resolutionMap[$parts[0]] || !($resolutionMap[$parts[0]] instanceof DataList)) {
1450
                        $field = $parts[0];
0 ignored issues
show
Unused Code introduced by
The assignment to $field is dead and can be removed.
Loading history...
1451
                        continue;
1452
                    }
1453
                    $relatedObject = get_class($resolutionMap[$parts[0]]);
1454
                    $tableName = $schema->tableForField($relatedObject, $parts[1]);
1455
                    $baseIDColumn = $schema->sqlColumnForField($dataClass, 'ID');
1456
                    $tableAlias = $parts[0];
1457
                    $dataList = $dataList->leftJoin($tableName, "\"{$tableAlias}\".\"ID\" = {$baseIDColumn}", $tableAlias);
1458
                }
1459
1460
                $sortSql[] = $field . ' ' . $dir;
1461
            }
1462
        }
1463
        if (!empty($sortSql)) {
1464
            $dataList = $dataList->sort(implode(", ", $sortSql));
1465
        }
1466
1467
        // Filtering is an array of field/type/value arrays
1468
        $where = [];
1469
        if ($filter) {
1470
            foreach ($filter as $filterValues) {
1471
                $cols = array_keys($this->columns);
1472
                $field = $filterValues['field'];
1473
                if (!in_array($field, $cols)) {
1474
                    throw new Exception("Invalid sort field: $field");
1475
                }
1476
                $value = $filterValues['value'];
1477
                $type = $filterValues['type'];
1478
1479
                // Strict value
1480
                if ($value === "true") {
1481
                    $value = true;
1482
                } elseif ($value === "false") {
1483
                    $value = false;
1484
                }
1485
1486
                switch ($type) {
1487
                    case "=":
1488
                        $where["$field"] = $value;
1489
                        break;
1490
                    case "!=":
1491
                        $where["$field:not"] = $value;
1492
                        break;
1493
                    case "like":
1494
                        $where["$field:PartialMatch:nocase"] = $value;
1495
                        break;
1496
                    case "keywords":
1497
                        $where["$field:PartialMatch:nocase"] = str_replace(" ", "%", $value);
1498
                        break;
1499
                    case "starts":
1500
                        $where["$field:StartsWith:nocase"] = $value;
1501
                        break;
1502
                    case "ends":
1503
                        $where["$field:EndsWith:nocase"] = $value;
1504
                        break;
1505
                    case "<":
1506
                        $where["$field:LessThan:nocase"] = $value;
1507
                        break;
1508
                    case "<=":
1509
                        $where["$field:LessThanOrEqual:nocase"] = $value;
1510
                        break;
1511
                    case ">":
1512
                        $where["$field:GreaterThan:nocase"] = $value;
1513
                        break;
1514
                    case ">=":
1515
                        $where["$field:GreaterThanOrEqual:nocase"] = $value;
1516
                        break;
1517
                    case "in":
1518
                        $where["$field"] = $value;
1519
                        break;
1520
                    case "regex":
1521
                        $dataList = $dataList->where('REGEXP ' . Convert::raw2sql($value));
1522
                        break;
1523
                    default:
1524
                        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...
1525
                }
1526
            }
1527
        }
1528
        if (!empty($where)) {
1529
            $dataList = $dataList->filter($where);
1530
        }
1531
1532
        $lastRow = $dataList->count();
1533
        $lastPage = ceil($lastRow / $limit);
1534
1535
        $data = [];
1536
        /** @var DataObject $record */
1537
        foreach ($dataList->limit($limit, $offset) as $record) {
1538
            if ($record->hasMethod('canView') && !$record->canView()) {
1539
                continue;
1540
            }
1541
1542
            $item = [
1543
                'ID' => $record->ID,
1544
            ];
1545
            $nested = [];
1546
            foreach ($this->columns as $col) {
1547
                if (empty($col['field'])) {
1548
                    continue;
1549
                }
1550
                $field = $col['field'];
1551
                if (strpos($field, '.') !== false) {
1552
                    $parts = explode('.', $field);
1553
                    $classOrField = $parts[0];
1554
                    $relationOrMethod = $parts[1];
1555
                    // For relations, like Users.count
1556
                    if ($singleton->getRelationClass($classOrField)) {
1557
                        $nested[$classOrField][] = $relationOrMethod;
1558
                        continue;
1559
                    } else {
1560
                        // For fields, like SomeValue.Nice
1561
                        $dbObject = $record->dbObject($classOrField);
1562
                        if ($dbObject) {
1563
                            $item[$classOrField] = [
1564
                                $relationOrMethod => $dbObject->$relationOrMethod()
1565
                            ];
1566
                            continue;
1567
                        }
1568
                    }
1569
                }
1570
                if (!isset($item[$field])) {
1571
                    $item[$field] = $record->getField($field);
1572
                }
1573
            }
1574
            // Fill in nested data, like Users.count
1575
            foreach ($nested as $nestedClass => $nestedColumns) {
1576
                /** @var DataObject $relObject */
1577
                $relObject = $record->relObject($nestedClass);
1578
                $nestedData = [];
1579
                foreach ($nestedColumns as $nestedColumn) {
1580
                    $nestedData[$nestedColumn] = $this->getDataFieldValue($relObject, $nestedColumn);
1581
                }
1582
                $item[$nestedClass] = $nestedData;
1583
            }
1584
            $data[] = $item;
1585
        }
1586
1587
        $result = [
1588
            'last_row' => $lastRow,
1589
            'last_page' => $lastPage,
1590
            'data' => $data,
1591
        ];
1592
1593
        return $result;
1594
    }
1595
1596
    public function getModelClass(): ?string
1597
    {
1598
        if ($this->modelClass) {
1599
            return $this->modelClass;
1600
        }
1601
        if ($this->list && $this->list instanceof DataList) {
1602
            return $this->list->dataClass();
1603
        }
1604
        return null;
1605
    }
1606
1607
    public function setModelClass(string $modelClass): self
1608
    {
1609
        $this->modelClass = $modelClass;
1610
        return $this;
1611
    }
1612
1613
1614
    public function getDataAttribute(string $k)
1615
    {
1616
        if (isset($this->dataAttributes[$k])) {
1617
            return $this->dataAttributes[$k];
1618
        }
1619
        return $this->getAttribute("data-$k");
1620
    }
1621
1622
    public function setDataAttribute(string $k, $v): self
1623
    {
1624
        $this->dataAttributes[$k] = $v;
1625
        return $this;
1626
    }
1627
1628
    public function dataAttributesHTML(): string
1629
    {
1630
        $parts = [];
1631
        foreach ($this->dataAttributes as $k => $v) {
1632
            if (!$v) {
1633
                continue;
1634
            }
1635
            if (is_array($v)) {
1636
                $v = json_encode($v);
1637
            }
1638
            $parts[] = "data-$k='$v'";
1639
        }
1640
        return implode(" ", $parts);
1641
    }
1642
1643
    protected function processLink(string $url): string
1644
    {
1645
        // It's not necessary to process
1646
        if ($url == '#') {
1647
            return $url;
1648
        }
1649
        // It's a temporary link on the form
1650
        if (strpos($url, 'form:') === 0) {
1651
            return $this->Link(preg_replace('/^form:/', '', $url));
1652
        }
1653
        // It's a temporary link on the controller
1654
        if (strpos($url, 'controller:') === 0) {
1655
            return $this->ControllerLink(preg_replace('/^controller:/', '', $url));
1656
        }
1657
        // It's a custom protocol (mailto: etc)
1658
        if (strpos($url, ':') !== false) {
1659
            return $url;
1660
        }
1661
        return $url;
1662
    }
1663
1664
    protected function processLinks(): void
1665
    {
1666
        // Process editor and formatter links
1667
        foreach ($this->columns as $name => $params) {
1668
            if (!empty($params['formatterParams']['url'])) {
1669
                $url = $this->processLink($params['formatterParams']['url']);
1670
                $this->columns[$name]['formatterParams']['url'] = $url;
1671
            }
1672
            if (!empty($params['editorParams']['url'])) {
1673
                $url = $this->processLink($params['editorParams']['url']);
1674
                $this->columns[$name]['editorParams']['url'] = $url;
1675
            }
1676
            // Set valuesURL automatically if not already set
1677
            if (!empty($params['editorParams']['autocomplete'])) {
1678
                if (empty($params['editorParams']['valuesURL'])) {
1679
                    $params = [
1680
                        'Column' => $name,
1681
                        'SecurityID' => SecurityToken::getSecurityID(),
1682
                    ];
1683
                    $url = $this->Link('autocomplete') . '?' . http_build_query($params);
1684
                    $this->columns[$name]['editorParams']['valuesURL'] = $url;
1685
                    $this->columns[$name]['editorParams']['filterRemote'] = true;
1686
                }
1687
            }
1688
        }
1689
1690
        // Other links
1691
        $url = $this->getOption('ajaxURL');
1692
        if ($url) {
1693
            $this->setOption('ajaxURL', $this->processLink($url));
1694
        }
1695
    }
1696
1697
    public function makeButton(string $urlOrAction, string $icon, string $title): array
1698
    {
1699
        $opts = [
1700
            "responsive" => 0,
1701
            "cssClass" => 'tabulator-cell-btn',
1702
            "tooltip" => $title,
1703
            "formatter" => "SSTabulator.buttonFormatter",
1704
            "formatterParams" => [
1705
                "icon" => $icon,
1706
                "title" => $title,
1707
                "url" => $this->TempLink($urlOrAction), // On the controller by default
1708
            ],
1709
            "cellClick" => "SSTabulator.buttonHandler",
1710
            "width" => 70,
1711
            "hozAlign" => "center",
1712
            "headerSort" => false,
1713
1714
        ];
1715
        return $opts;
1716
    }
1717
1718
    public function addButtonFromArray(string $action, array $opts = [], string $before = null): self
1719
    {
1720
        // Insert before given column
1721
        if ($before) {
1722
            if (array_key_exists($before, $this->columns)) {
1723
                $new = [];
1724
                foreach ($this->columns as $k => $value) {
1725
                    if ($k === $before) {
1726
                        $new["action_$action"] = $opts;
1727
                    }
1728
                    $new[$k] = $value;
1729
                }
1730
                $this->columns = $new;
1731
            }
1732
        } else {
1733
            $this->columns["action_$action"] = $opts;
1734
        }
1735
        return $this;
1736
    }
1737
1738
    /**
1739
     * @param string $action Action name
1740
     * @param string $url Parameters between {} will be interpolated by row values.
1741
     * @param string $icon
1742
     * @param string $title
1743
     * @param string|null $before
1744
     * @return self
1745
     */
1746
    public function addButton(string $action, string $url, string $icon, string $title, string $before = null): self
1747
    {
1748
        $opts = $this->makeButton($url, $icon, $title);
1749
        $this->addButtonFromArray($action, $opts, $before);
1750
        return $this;
1751
    }
1752
1753
    public function shiftButton(string $action, string $url, string $icon, string $title): self
1754
    {
1755
        // Find first action
1756
        foreach ($this->columns as $name => $options) {
1757
            if (strpos($name, 'action_') === 0) {
1758
                return $this->addButton($action, $url, $icon, $title, $name);
1759
            }
1760
        }
1761
        return $this->addButton($action, $url, $icon, $title);
1762
    }
1763
1764
    public function removeButton(string $action): self
1765
    {
1766
        if (isset($this->columns["action_$action"])) {
1767
            unset($this->columns["action_$action"]);
1768
        }
1769
        return $this;
1770
    }
1771
1772
    /**
1773
     * @link http://www.tabulator.info/docs/5.4/columns#definition
1774
     * @param string $field (Required) this is the key for this column in the data array
1775
     * @param string $title (Required) This is the title that will be displayed in the header for this column
1776
     * @param array $opts Other options to merge in
1777
     * @return $this
1778
     */
1779
    public function addColumn(string $field, string $title = null, array $opts = []): self
1780
    {
1781
        if ($title === null) {
1782
            $title = $field;
1783
        }
1784
1785
        $baseOpts = [
1786
            "field" => $field,
1787
            "title" => $title,
1788
        ];
1789
1790
        if (!empty($opts)) {
1791
            $baseOpts = array_merge($baseOpts, $opts);
1792
        }
1793
1794
        $this->columns[$field] = $baseOpts;
1795
        return $this;
1796
    }
1797
1798
    /**
1799
     * @link http://www.tabulator.info/docs/5.4/columns#definition
1800
     * @param array $opts Other options to merge in
1801
     * @return $this
1802
     */
1803
    public function addColumnFromArray(array $opts = [])
1804
    {
1805
        if (empty($opts['field']) || !isset($opts['title'])) {
1806
            throw new Exception("Missing field or title key");
1807
        }
1808
        $field = $opts['field'];
1809
        $this->columns[$field] = $opts;
1810
        return $this;
1811
    }
1812
1813
    public function makeColumnEditable(string $field, string $editor = "input", array $params = [])
1814
    {
1815
        $col = $this->getColumn($field);
1816
        if (!$col) {
1817
            throw new InvalidArgumentException("$field is not a valid column");
1818
        }
1819
1820
        switch ($editor) {
1821
            case 'date':
1822
                $editor = "input";
1823
                $params = [
1824
                    'mask' => "9999-99-99",
1825
                    'maskAutoFill' => 'true',
1826
                ];
1827
                break;
1828
            case 'datetime':
1829
                $editor = "input";
1830
                $params = [
1831
                    'mask' => "9999-99-99 99:99:99",
1832
                    'maskAutoFill' => 'true',
1833
                ];
1834
                break;
1835
        }
1836
1837
        if (empty($col['cssClass'])) {
1838
            $col['cssClass'] = 'no-change-track';
1839
        } else {
1840
            $col['cssClass'] .= ' no-change-track';
1841
        }
1842
1843
        $col['editor'] = $editor;
1844
        $col['editorParams'] = $params;
1845
        if ($editor == "list") {
1846
            if (!empty($params['autocomplete'])) {
1847
                $col['headerFilter'] = "input"; // force input
1848
            } else {
1849
                $col['headerFilterParams'] = $params; // editor is used as base filter editor
1850
            }
1851
        }
1852
1853
1854
        $this->setColumn($field, $col);
1855
    }
1856
1857
    /**
1858
     * Get column details
1859
1860
     * @param string $key
1861
     */
1862
    public function getColumn(string $key): ?array
1863
    {
1864
        if (isset($this->columns[$key])) {
1865
            return $this->columns[$key];
1866
        }
1867
        return null;
1868
    }
1869
1870
    /**
1871
     * Set column details
1872
     *
1873
     * @param string $key
1874
     * @param array $col
1875
     */
1876
    public function setColumn(string $key, array $col): self
1877
    {
1878
        $this->columns[$key] = $col;
1879
        return $this;
1880
    }
1881
1882
    /**
1883
     * Update column details
1884
     *
1885
     * @param string $key
1886
     * @param array $col
1887
     */
1888
    public function updateColumn(string $key, array $col): self
1889
    {
1890
        $data = $this->getColumn($key);
1891
        if ($data) {
1892
            $this->setColumn($key, array_merge($data, $col));
1893
        }
1894
        return $this;
1895
    }
1896
1897
    /**
1898
     * Remove a column
1899
     *
1900
     * @param string $key
1901
     */
1902
    public function removeColumn(string $key): void
1903
    {
1904
        unset($this->columns[$key]);
1905
    }
1906
1907
1908
    /**
1909
     * Get the value of columns
1910
     */
1911
    public function getColumns(): array
1912
    {
1913
        return $this->columns;
1914
    }
1915
1916
    /**
1917
     * Set the value of columns
1918
     */
1919
    public function setColumns(array $columns): self
1920
    {
1921
        $this->columns = $columns;
1922
        return $this;
1923
    }
1924
1925
    /**
1926
     * @param string|AbstractTabulatorTool $tool Pass name or class
1927
     * @return AbstractTabulatorTool|null
1928
     */
1929
    public function getTool($tool): ?AbstractTabulatorTool
1930
    {
1931
        if (is_object($tool)) {
1932
            $tool = get_class($tool);
1933
        }
1934
        if (!is_string($tool)) {
0 ignored issues
show
introduced by
The condition is_string($tool) is always true.
Loading history...
1935
            throw new InvalidArgumentException('Tool must be an object or a class name');
1936
        }
1937
        foreach ($this->tools as $t) {
1938
            if ($t['name'] === $tool) {
1939
                return $t['tool'];
1940
            }
1941
            if ($t['tool'] instanceof $tool) {
1942
                return $t['tool'];
1943
            }
1944
        }
1945
        return null;
1946
    }
1947
1948
    public function addTool(string $pos, AbstractTabulatorTool $tool, string $name = ''): self
1949
    {
1950
        $tool->setTabulatorGrid($this);
1951
        $tool->setName($name);
1952
1953
        $this->tools[] = [
1954
            'position' => $pos,
1955
            'tool' => $tool,
1956
            'name' => $name,
1957
        ];
1958
        return $this;
1959
    }
1960
1961
    public function removeTool($tool): self
1962
    {
1963
        if (is_object($tool)) {
1964
            $tool = get_class($tool);
1965
        }
1966
        if (!is_string($tool)) {
1967
            throw new InvalidArgumentException('Tool must be an object or a class name');
1968
        }
1969
        foreach ($this->tools as $idx => $tool) {
0 ignored issues
show
introduced by
$tool is overwriting one of the parameters of this function.
Loading history...
1970
            if ($tool['name'] === $tool) {
1971
                unset($this->tools[$idx]);
1972
            }
1973
            if ($tool['tool'] instanceof $tool) {
1974
                unset($this->tools[$idx]);
1975
            }
1976
        }
1977
        return $this;
1978
    }
1979
1980
    /**
1981
     * @param string|AbstractBulkAction $bulkAction Pass name or class
1982
     * @return AbstractBulkAction|null
1983
     */
1984
    public function getBulkAction($bulkAction): ?AbstractBulkAction
1985
    {
1986
        if (is_object($bulkAction)) {
1987
            $bulkAction = get_class($bulkAction);
1988
        }
1989
        if (!is_string($bulkAction)) {
0 ignored issues
show
introduced by
The condition is_string($bulkAction) is always true.
Loading history...
1990
            throw new InvalidArgumentException('BulkAction must be an object or a class name');
1991
        }
1992
        foreach ($this->bulkActions as $ba) {
1993
            if ($ba->getName() == $bulkAction) {
1994
                return $ba;
1995
            }
1996
            if ($ba instanceof $bulkAction) {
1997
                return $ba;
1998
            }
1999
        }
2000
        return null;
2001
    }
2002
2003
    public function getBulkActions(): array
2004
    {
2005
        return $this->bulkActions;
2006
    }
2007
2008
    /**
2009
     * @param AbstractBulkAction[] $bulkActions
2010
     * @return self
2011
     */
2012
    public function setBulkActions(array $bulkActions): self
2013
    {
2014
        foreach ($bulkActions as $bulkAction) {
2015
            $bulkAction->setTabulatorGrid($this);
2016
        }
2017
        $this->bulkActions = $bulkActions;
2018
        return $this;
2019
    }
2020
2021
    public function addBulkAction(AbstractBulkAction $handler): self
2022
    {
2023
        $handler->setTabulatorGrid($this);
2024
2025
        $this->bulkActions[] = $handler;
2026
        return $this;
2027
    }
2028
2029
    public function removeBulkAction($bulkAction): self
2030
    {
2031
        if (is_object($bulkAction)) {
2032
            $bulkAction = get_class($bulkAction);
2033
        }
2034
        if (!is_string($bulkAction)) {
2035
            throw new InvalidArgumentException('Bulk action must be an object or a class name');
2036
        }
2037
        foreach ($this->bulkActions as $idx => $ba) {
2038
            if ($ba->getName() == $bulkAction) {
2039
                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...
2040
            }
2041
            if ($ba instanceof $bulkAction) {
2042
                unset($this->bulkAction[$idx]);
2043
            }
2044
        }
2045
        return $this;
2046
    }
2047
2048
    public function getColumnDefault(string $opt)
2049
    {
2050
        return $this->columnDefaults[$opt] ?? null;
2051
    }
2052
2053
    public function setColumnDefault(string $opt, $value)
2054
    {
2055
        $this->columnDefaults[$opt] = $value;
2056
    }
2057
2058
    public function getColumnDefaults(): array
2059
    {
2060
        return $this->columnDefaults;
2061
    }
2062
2063
    public function setColumnDefaults(array $columnDefaults): self
2064
    {
2065
        $this->columnDefaults = $columnDefaults;
2066
        return $this;
2067
    }
2068
2069
    public function getListeners(): array
2070
    {
2071
        return $this->listeners;
2072
    }
2073
2074
    public function setListeners(array $listeners): self
2075
    {
2076
        $this->listeners = $listeners;
2077
        return $this;
2078
    }
2079
2080
    public function addListener(string $event, string $functionName): self
2081
    {
2082
        $this->listeners[$event] = $functionName;
2083
        return $this;
2084
    }
2085
2086
    public function removeListener(string $event): self
2087
    {
2088
        if (isset($this->listeners[$event])) {
2089
            unset($this->listeners[$event]);
2090
        }
2091
        return $this;
2092
    }
2093
2094
    public function getJsNamespaces(): array
2095
    {
2096
        return $this->jsNamespaces;
2097
    }
2098
2099
    public function setJsNamespaces(array $jsNamespaces): self
2100
    {
2101
        $this->jsNamespaces = $jsNamespaces;
2102
        return $this;
2103
    }
2104
2105
    public function registerJsNamespace(string $ns): self
2106
    {
2107
        $this->jsNamespaces[] = $ns;
2108
        return $this;
2109
    }
2110
2111
    public function unregisterJsNamespace(string $ns): self
2112
    {
2113
        $this->jsNamespaces = array_diff($this->jsNamespaces, [$ns]);
2114
        return $this;
2115
    }
2116
2117
    public function getLinksOptions(): array
2118
    {
2119
        return $this->linksOptions;
2120
    }
2121
2122
    public function setLinksOptions(array $linksOptions): self
2123
    {
2124
        $this->linksOptions = $linksOptions;
2125
        return $this;
2126
    }
2127
2128
    public function registerLinkOption(string $linksOption): self
2129
    {
2130
        $this->linksOptions[] = $linksOption;
2131
        return $this;
2132
    }
2133
2134
    public function unregisterLinkOption(string $linksOption): self
2135
    {
2136
        $this->linksOptions = array_diff($this->linksOptions, [$linksOption]);
2137
        return $this;
2138
    }
2139
2140
    /**
2141
     * Get the value of pageSize
2142
     */
2143
    public function getPageSize(): int
2144
    {
2145
        return $this->pageSize;
2146
    }
2147
2148
    /**
2149
     * Set the value of pageSize
2150
     *
2151
     * @param int $pageSize
2152
     */
2153
    public function setPageSize(int $pageSize): self
2154
    {
2155
        $this->pageSize = $pageSize;
2156
        return $this;
2157
    }
2158
2159
    /**
2160
     * Get the value of autoloadDataList
2161
     */
2162
    public function getAutoloadDataList(): bool
2163
    {
2164
        return $this->autoloadDataList;
2165
    }
2166
2167
    /**
2168
     * Set the value of autoloadDataList
2169
     *
2170
     * @param bool $autoloadDataList
2171
     */
2172
    public function setAutoloadDataList(bool $autoloadDataList): self
2173
    {
2174
        $this->autoloadDataList = $autoloadDataList;
2175
        return $this;
2176
    }
2177
2178
    /**
2179
     * Set the value of itemRequestClass
2180
     */
2181
    public function setItemRequestClass(string $itemRequestClass): self
2182
    {
2183
        $this->itemRequestClass = $itemRequestClass;
2184
        return $this;
2185
    }
2186
2187
    /**
2188
     * Get the value of lazyInit
2189
     */
2190
    public function getLazyInit(): bool
2191
    {
2192
        return $this->lazyInit;
2193
    }
2194
2195
    /**
2196
     * Set the value of lazyInit
2197
     */
2198
    public function setLazyInit(bool $lazyInit): self
2199
    {
2200
        $this->lazyInit = $lazyInit;
2201
        return $this;
2202
    }
2203
2204
    /**
2205
     * Get the value of rowClickTriggersAction
2206
     */
2207
    public function getRowClickTriggersAction(): bool
2208
    {
2209
        return $this->rowClickTriggersAction;
2210
    }
2211
2212
    /**
2213
     * Set the value of rowClickTriggersAction
2214
     */
2215
    public function setRowClickTriggersAction(bool $rowClickTriggersAction): self
2216
    {
2217
        $this->rowClickTriggersAction = $rowClickTriggersAction;
2218
        return $this;
2219
    }
2220
2221
    /**
2222
     * Get the value of controllerFunction
2223
     */
2224
    public function getControllerFunction(): string
2225
    {
2226
        if (!$this->controllerFunction) {
2227
            return $this->getName() ?? "TabulatorGrid";
2228
        }
2229
        return $this->controllerFunction;
2230
    }
2231
2232
    /**
2233
     * Set the value of controllerFunction
2234
     */
2235
    public function setControllerFunction(string $controllerFunction): self
2236
    {
2237
        $this->controllerFunction = $controllerFunction;
2238
        return $this;
2239
    }
2240
2241
    /**
2242
     * Get the value of useConfigProvider
2243
     */
2244
    public function getUseConfigProvider(): bool
2245
    {
2246
        return $this->useConfigProvider;
2247
    }
2248
2249
    /**
2250
     * Set the value of useConfigProvider
2251
     */
2252
    public function setUseConfigProvider(bool $useConfigProvider): self
2253
    {
2254
        $this->useConfigProvider = $useConfigProvider;
2255
        return $this;
2256
    }
2257
2258
    /**
2259
     * Get the value of useInitScript
2260
     */
2261
    public function getUseInitScript(): bool
2262
    {
2263
        return $this->useConfigProvider;
2264
    }
2265
2266
    /**
2267
     * Set the value of useInitScript
2268
     */
2269
    public function setUseInitScript(bool $useInitScript): self
2270
    {
2271
        $this->useInitScript = $useInitScript;
2272
        return $this;
2273
    }
2274
2275
    /**
2276
     * Get the value of editUrl
2277
     */
2278
    public function getEditUrl(): string
2279
    {
2280
        return $this->editUrl;
2281
    }
2282
2283
    /**
2284
     * Set the value of editUrl
2285
     */
2286
    public function setEditUrl(string $editUrl): self
2287
    {
2288
        $this->editUrl = $editUrl;
2289
        return $this;
2290
    }
2291
2292
    /**
2293
     * Get the value of moveUrl
2294
     */
2295
    public function getMoveUrl(): string
2296
    {
2297
        return $this->moveUrl;
2298
    }
2299
2300
    /**
2301
     * Set the value of moveUrl
2302
     */
2303
    public function setMoveUrl(string $moveUrl): self
2304
    {
2305
        $this->moveUrl = $moveUrl;
2306
        return $this;
2307
    }
2308
2309
    /**
2310
     * Get the value of bulkUrl
2311
     */
2312
    public function getBulkUrl(): string
2313
    {
2314
        return $this->bulkUrl;
2315
    }
2316
2317
    /**
2318
     * Set the value of bulkUrl
2319
     */
2320
    public function setBulkUrl(string $bulkUrl): self
2321
    {
2322
        $this->bulkUrl = $bulkUrl;
2323
        return $this;
2324
    }
2325
}
2326