Passed
Push — master ( db7ee9...816430 )
by Thomas
16:37
created

TabulatorGrid::setColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 2
c 2
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
    protected bool $globalSearch = false;
256
257
    protected array $wildcardFields = [];
258
259
    protected array $quickFilters = [];
260
261
    /**
262
     * @param string $fieldName
263
     * @param string|null|bool $title
264
     * @param SS_List $value
265
     */
266
    public function __construct($name, $title = null, $value = null)
267
    {
268
        parent::__construct($name, $title, $value);
269
        $this->options = self::config()->default_options ?? [];
270
        $this->columnDefaults = self::config()->default_column_options ?? [];
271
        $this->setLazyInit(self::config()->default_lazy_init);
272
273
        // We don't want regular setValue for this since it would break with loadFrom logic
274
        if ($value) {
275
            $this->setList($value);
276
        }
277
    }
278
279
    /**
280
     * This helps if some third party code expects the TabulatorGrid to be a GridField
281
     * Only works to a really basic extent
282
     */
283
    public function getConfig(): GridFieldConfig
284
    {
285
        return new GridFieldConfig;
286
    }
287
288
    /**
289
     * This helps if some third party code expects the TabulatorGrid to be a GridField
290
     * Only works to a really basic extent
291
     */
292
    public function setConfig($config)
293
    {
294
        // ignore
295
    }
296
297
    /**
298
     * Temporary link that will be replaced by a real link by processLinks
299
     *
300
     * @param string $action
301
     * @return string
302
     */
303
    public function TempLink(string $action, bool $controller = true): string
304
    {
305
        // It's an absolute link
306
        if (strpos($action, '/') === 0 || strpos($action, 'http') === 0) {
307
            return $action;
308
        }
309
        // Already temp
310
        if (strpos($action, ':') !== false) {
311
            return $action;
312
        }
313
        $prefix = $controller ? "controller" : "form";
314
        return "$prefix:$action";
315
    }
316
317
    public function ControllerLink(string $action): string
318
    {
319
        return $this->getForm()->getController()->Link($action);
320
    }
321
322
    public function getCreateLink(): string
323
    {
324
        return Controller::join_links($this->Link('item'), 'new');
325
    }
326
327
    /**
328
     * @param FieldList $fields
329
     * @param string $name
330
     * @return TabulatorGrid|null
331
     */
332
    public static function replaceGridField(FieldList $fields, string $name)
333
    {
334
        /** @var \SilverStripe\Forms\GridField\GridField $gridField */
335
        $gridField = $fields->dataFieldByName($name);
336
        if (!$gridField) {
0 ignored issues
show
introduced by
$gridField is of type SilverStripe\Forms\GridField\GridField, thus it always evaluated to true.
Loading history...
337
            return;
338
        }
339
        if ($gridField instanceof TabulatorGrid) {
0 ignored issues
show
introduced by
$gridField is never a sub-type of LeKoala\Tabulator\TabulatorGrid.
Loading history...
340
            return $gridField;
341
        }
342
        $tabulatorGrid = new TabulatorGrid($name, $gridField->Title(), $gridField->getList());
343
        // In the cms, this is mostly never happening
344
        if ($gridField->getForm()) {
345
            $tabulatorGrid->setForm($gridField->getForm());
346
        }
347
        $tabulatorGrid->configureFromDataObject($gridField->getModelClass());
348
        $tabulatorGrid->setLazyInit(true);
349
        $fields->replaceField($name, $tabulatorGrid);
350
351
        return $tabulatorGrid;
352
    }
353
354
    public function configureFromDataObject($className = null, bool $clear = true): void
355
    {
356
        $this->columns = [];
357
358
        if (!$className) {
359
            $className = $this->getModelClass();
360
        }
361
        if (!$className) {
362
            throw new RuntimeException("Could not find the model class");
363
        }
364
        $this->modelClass = $className;
365
366
        /** @var DataObject $singl */
367
        $singl = singleton($className);
368
369
        // Mock some base columns using SilverStripe built-in methods
370
        $columns = [];
371
372
        foreach ($singl->summaryFields() as $field => $title) {
373
            // Deal with this in load() instead
374
            // if (strpos($field, '.') !== false) {
375
            // $fieldParts = explode(".", $field);
376
377
            // It can be a relation Users.Count or a field Field.Nice
378
            // $classOrField = $fieldParts[0];
379
            // $relationOrMethod = $fieldParts[1];
380
            // }
381
            $title = str_replace(".", " ", $title);
382
            $columns[$field] = [
383
                'field' => $field,
384
                'title' => $title,
385
            ];
386
387
            $dbObject = $singl->dbObject($field);
388
            if ($dbObject) {
389
                if ($dbObject instanceof DBBoolean) {
390
                    $columns[$field]['formatter'] = "SSTabulator.customTickCrossFormatter";
391
                }
392
            }
393
        }
394
        foreach ($singl->searchableFields() as $key => $searchOptions) {
395
            /*
396
            "filter" => "NameOfTheFilter"
397
            "field" => "SilverStripe\Forms\FormField"
398
            "title" => "Title of the field"
399
            */
400
            if (!isset($columns[$key])) {
401
                continue;
402
            }
403
            $columns[$key]['headerFilter'] = true;
404
            // $columns[$key]['headerFilterPlaceholder'] = $searchOptions['title'];
405
            //TODO: implement filter mapping
406
            switch ($searchOptions['filter']) {
407
                default:
408
                    $columns[$key]['headerFilterFunc'] =  "like";
409
                    break;
410
            }
411
412
            // Restrict based on data type
413
            $dbObject = $singl->dbObject($key);
414
            if ($dbObject) {
415
                if ($dbObject instanceof DBBoolean) {
416
                    $columns[$key]['headerFilter'] = 'tickCross';
417
                    $columns[$key]['headerFilterFunc'] =  "=";
418
                    $columns[$key]['headerFilterParams'] =  [
419
                        'tristate' => true
420
                    ];
421
                }
422
                if ($dbObject instanceof DBEnum) {
423
                    $columns[$key]['headerFilter'] = 'list';
424
                    $columns[$key]['headerFilterFunc'] =  "=";
425
                    $columns[$key]['headerFilterParams'] =  [
426
                        'values' => $dbObject->enumValues()
427
                    ];
428
                }
429
            }
430
        }
431
432
        // Allow customizing our columns based on record
433
        if ($singl->hasMethod('tabulatorColumns')) {
434
            $fields = $singl->tabulatorColumns();
435
            if (!is_array($fields)) {
436
                throw new RuntimeException("tabulatorColumns must return an array");
437
            }
438
            foreach ($fields as $key => $columnOptions) {
439
                $baseOptions = $columns[$key] ?? [];
440
                $columns[$key] = array_merge($baseOptions, $columnOptions);
441
            }
442
        }
443
444
        $this->extend('updateConfiguredColumns', $columns);
445
446
        foreach ($columns as $col) {
447
            $this->addColumn($col['field'], $col['title'], $col);
448
        }
449
450
        // Sortable ?
451
        if ($singl->hasField('Sort')) {
452
            $this->wizardMoveable();
453
        }
454
455
        // Actions
456
        // We use a pseudo link, because maybe we cannot call Link() yet if it's not linked to a form
457
458
        $this->bulkUrl = $this->TempLink("bulkAction/", false);
459
460
        // - Core actions, handled by TabulatorGrid
461
        $itemUrl = $this->TempLink('item/{ID}', false);
462
        if ($singl->canEdit()) {
463
            $this->addButton("ui_edit", $itemUrl, "edit", "Edit");
464
            $this->editUrl = $this->TempLink("item/{ID}/ajaxEdit", false);
465
        } elseif ($singl->canView()) {
466
            $this->addButton("ui_view", $itemUrl, "visibility", "View");
467
        }
468
469
        // - Tools
470
        $this->tools = [];
471
        if ($singl->canCreate()) {
472
            $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

472
            $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...
473
        }
474
        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...
475
            $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

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

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

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

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

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

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

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

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