Passed
Push — master ( 354ef3...e55602 )
by Thomas
03:13
created

TabulatorGrid::setState()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

454
            $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...
455
        }
456
        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...
457
            $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

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

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

1081
        $response = new HTTPResponse(/** @scrutinizer ignore-deprecated */ $this->getInitScript());
Loading history...
1082
        $response->addHeader('Content-Type', 'application/script');
1083
        return $response;
1084
    }
1085
1086
    /**
1087
     * Provides autocomplete lists
1088
     *
1089
     * @param HTTPRequest $request
1090
     * @return HTTPResponse
1091
     */
1092
    public function autocomplete(HTTPRequest $request)
1093
    {
1094
        if ($this->isDisabled() || $this->isReadonly()) {
1095
            return $this->httpError(403);
1096
        }
1097
        $SecurityID = $request->getVar('SecurityID');
1098
        if (!SecurityToken::inst()->check($SecurityID)) {
1099
            return $this->httpError(404, "Invalid SecurityID");
1100
        }
1101
1102
        $name = $request->getVar("Column");
1103
        $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

1103
        $col = $this->getColumn(/** @scrutinizer ignore-type */ $name);
Loading history...
1104
        if (!$col) {
1105
            return $this->httpError(403, "Invalid column");
1106
        }
1107
1108
        // Don't use % term as it prevents use of indexes
1109
        $term = $request->getVar('term') . '%';
1110
        $term = str_replace(' ', '%', $term);
1111
1112
        $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

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

1350
            throw new RuntimeException("Value is not a ArrayList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1351
        }
1352
        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...
1353
    }
1354
1355
    public function hasDataList(): bool
1356
    {
1357
        return $this->list instanceof DataList;
1358
    }
1359
1360
    /**
1361
     * A properly typed on which you can call byID
1362
     * @return ArrayList|DataList
1363
     */
1364
    public function getByIDList()
1365
    {
1366
        return $this->list;
1367
    }
1368
1369
    public function hasByIDList(): bool
1370
    {
1371
        return $this->hasDataList() || $this->hasArrayList();
1372
    }
1373
1374
    public function getDataList(): DataList
1375
    {
1376
        if (!$this->list instanceof DataList) {
1377
            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

1377
            throw new RuntimeException("Value is not a DataList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1378
        }
1379
        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...
1380
    }
1381
1382
    public function getManipulatedData(int $limit, int $offset, array $sort = null, array $filter = null): array
1383
    {
1384
        if (!$this->hasDataList()) {
1385
            $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

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