Passed
Push — master ( aec283...98a649 )
by Thomas
22:11 queued 11:07
created

TabulatorGrid::getTool()   A

Complexity

Conditions 6
Paths 10

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 17
rs 9.2222
cc 6
nc 10
nop 1
1
<?php
2
3
namespace LeKoala\Tabulator;
4
5
use Exception;
6
use RuntimeException;
7
use SilverStripe\i18n\i18n;
8
use SilverStripe\Forms\Form;
9
use InvalidArgumentException;
10
use 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
     * Temporary link that will be replaced by a real link by processLinks
284
     *
285
     * @param string $action
286
     * @return string
287
     */
288
    public function TempLink(string $action, bool $controller = true): string
289
    {
290
        // It's an absolute link
291
        if (strpos($action, '/') === 0 || strpos($action, 'http') === 0) {
292
            return $action;
293
        }
294
        // Already temp
295
        if (strpos($action, ':') !== false) {
296
            return $action;
297
        }
298
        $prefix = $controller ? "controller" : "form";
299
        return "$prefix:$action";
300
    }
301
302
    public function ControllerLink(string $action): string
303
    {
304
        return $this->getForm()->getController()->Link($action);
305
    }
306
307
    public function getCreateLink(): string
308
    {
309
        return Controller::join_links($this->Link('item'), 'new');
310
    }
311
312
    /**
313
     * @param FieldList $fields
314
     * @param string $name
315
     * @return TabulatorGrid|null
316
     */
317
    public static function replaceGridField(FieldList $fields, string $name)
318
    {
319
        /** @var \SilverStripe\Forms\GridField\GridField $gridField */
320
        $gridField = $fields->dataFieldByName($name);
321
        if (!$gridField) {
0 ignored issues
show
introduced by
$gridField is of type SilverStripe\Forms\GridField\GridField, thus it always evaluated to true.
Loading history...
322
            return;
323
        }
324
        $tabulatorGrid = new TabulatorGrid($name, $gridField->Title(), $gridField->getList());
325
        // In the cms, this is mostly never happening
326
        if ($gridField->getForm()) {
327
            $tabulatorGrid->setForm($gridField->getForm());
328
        }
329
        $tabulatorGrid->configureFromDataObject($gridField->getModelClass());
330
        $tabulatorGrid->setLazyInit(true);
331
        $fields->replaceField($name, $tabulatorGrid);
332
333
        return $tabulatorGrid;
334
    }
335
336
    public function configureFromDataObject($className = null, bool $clear = true): void
337
    {
338
        $this->columns = [];
339
340
        if (!$className) {
341
            $className = $this->getModelClass();
342
        }
343
        if (!$className) {
344
            throw new RuntimeException("Could not find the model class");
345
        }
346
        $this->modelClass = $className;
347
348
        /** @var DataObject $singl */
349
        $singl = singleton($className);
350
351
        // Mock some base columns using SilverStripe built-in methods
352
        $columns = [];
353
        foreach ($singl->summaryFields() as $field => $title) {
354
            $title = str_replace(".", " ", $title);
355
            $columns[$field] = [
356
                'field' => $field,
357
                'title' => $title,
358
            ];
359
360
            $dbObject = $singl->dbObject($field);
361
            if ($dbObject) {
362
                if ($dbObject instanceof DBBoolean) {
363
                    $columns[$field]['formatter'] = "SSTabulator.customTickCrossFormatter";
364
                }
365
            }
366
        }
367
        foreach ($singl->searchableFields() as $key => $searchOptions) {
368
            /*
369
            "filter" => "NameOfTheFilter"
370
            "field" => "SilverStripe\Forms\FormField"
371
            "title" => "Title of the field"
372
            */
373
            if (!isset($columns[$key])) {
374
                continue;
375
            }
376
            $columns[$key]['headerFilter'] = true;
377
            // $columns[$key]['headerFilterPlaceholder'] = $searchOptions['title'];
378
            //TODO: implement filter mapping
379
            switch ($searchOptions['filter']) {
380
                default:
381
                    $columns[$key]['headerFilterFunc'] =  "like";
382
                    break;
383
            }
384
385
            // Restrict based on data type
386
            $dbObject = $singl->dbObject($key);
387
            if ($dbObject) {
388
                if ($dbObject instanceof DBBoolean) {
389
                    $columns[$key]['headerFilter'] = 'tickCross';
390
                    $columns[$key]['headerFilterFunc'] =  "=";
391
                    $columns[$key]['headerFilterParams'] =  [
392
                        'tristate' => true
393
                    ];
394
                }
395
                if ($dbObject instanceof DBEnum) {
396
                    $columns[$key]['headerFilter'] = 'list';
397
                    $columns[$key]['headerFilterFunc'] =  "=";
398
                    $columns[$key]['headerFilterParams'] =  [
399
                        'values' => $dbObject->enumValues()
400
                    ];
401
                }
402
            }
403
        }
404
405
        // Allow customizing our columns based on record
406
        if ($singl->hasMethod('tabulatorColumns')) {
407
            $fields = $singl->tabulatorColumns();
408
            if (!is_array($fields)) {
409
                throw new RuntimeException("tabulatorColumns must return an array");
410
            }
411
            foreach ($fields as $key => $columnOptions) {
412
                $baseOptions = $columns[$key] ?? [];
413
                $columns[$key] = array_merge($baseOptions, $columnOptions);
414
            }
415
        }
416
417
        $this->extend('updateConfiguredColumns', $columns);
418
419
        foreach ($columns as $col) {
420
            $this->addColumn($col['field'], $col['title'], $col);
421
        }
422
423
        // Sortable ?
424
        if ($singl->hasField('Sort')) {
425
            $this->wizardMoveable();
426
        }
427
428
        // Actions
429
        // We use a pseudo link, because maybe we cannot call Link() yet if it's not linked to a form
430
431
        $this->bulkUrl = $this->TempLink("bulkAction/", false);
432
433
        // - Core actions, handled by TabulatorGrid
434
        $itemUrl = $this->TempLink('item/{ID}', false);
435
        if ($singl->canEdit()) {
436
            $this->addButton("ui_edit", $itemUrl, "edit", "Edit");
437
            $this->editUrl = $this->TempLink("item/{ID}/ajaxEdit", false);
438
        } elseif ($singl->canView()) {
439
            $this->addButton("ui_view", $itemUrl, "visibility", "View");
440
        }
441
442
        // - Tools
443
        $this->tools = [];
444
        if ($singl->canCreate()) {
445
            $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

445
            $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...
446
        }
447
        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...
448
            $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

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

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

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

1094
        $col = $this->getColumn(/** @scrutinizer ignore-type */ $name);
Loading history...
1095
        if (!$col) {
1096
            return $this->httpError(403, "Invalid column");
1097
        }
1098
1099
        // Don't use % term as it prevents use of indexes
1100
        $term = $request->getVar('term') . '%';
1101
        $term = str_replace(' ', '%', $term);
1102
1103
        $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

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

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

1368
            throw new RuntimeException("Value is not a DataList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1369
        }
1370
        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...
1371
    }
1372
1373
    public function getManipulatedData(int $limit, int $offset, array $sort = null, array $filter = null): array
1374
    {
1375
        if (!$this->hasDataList()) {
1376
            $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

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