BaseElement   F
last analyzed

Complexity

Total Complexity 115

Size/Duplication

Total Lines 987
Duplicated Lines 0 %

Importance

Changes 15
Bugs 0 Features 0
Metric Value
eloc 317
c 15
b 0
f 0
dl 0
loc 987
rs 2
wmc 115

46 Methods

Rating   Name   Duplication   Size   Complexity  
A canDelete() 0 14 5
A canView() 0 14 5
A canEdit() 0 14 5
A canCreate() 0 8 3
A provideBlockSchema() 0 8 1
A Pos() 0 3 1
A getAuthor() 0 7 2
A First() 0 3 1
A sanitiseClassName() 0 3 1
A getBlockSchema() 0 7 1
A TotalItems() 0 3 1
A getPageTitle() 0 9 2
A getPage() 0 16 5
A getEditorPreview() 0 6 1
A Top() 0 3 2
A getController() 0 18 3
A unsanitiseClassName() 0 3 1
A getTypeNice() 0 8 2
A getBlockConfig() 0 3 1
A getSummary() 0 3 1
A AbsoluteLink() 0 9 2
A getDescription() 0 7 2
A stripNamespacing() 0 4 1
B getAreaRelationName() 0 29 8
A PageCMSEditLink() 0 11 2
A getMimeType() 0 3 1
B CMSEditLink() 0 35 7
A getRenderTemplates() 0 32 6
A Last() 0 3 1
A getStyleVariant() 0 14 2
A Link() 0 11 2
A PreviewLink() 0 7 1
A getType() 0 3 1
B getAnchor() 0 33 8
A EvenOdd() 0 5 2
A setController() 0 5 1
A getSimpleClassName() 0 3 1
A setAreaRelationNameCache() 0 5 1
A updateFromFormData() 0 13 3
A getCMSFields() 0 81 4
A isCMSPreview() 0 11 3
A getEditLink() 0 3 1
A forTemplate() 0 9 2
A onBeforeWrite() 0 14 3
A getIcon() 0 30 5
A inlineEditable() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like BaseElement often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BaseElement, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace DNADesign\Elemental\Models;
4
5
use DNADesign\Elemental\Controllers\ElementController;
6
use DNADesign\Elemental\Forms\TextCheckboxGroupField;
7
use DNADesign\Elemental\ORM\FieldType\DBObjectType;
8
use Exception;
9
use SilverStripe\CMS\Controllers\CMSPageEditController;
10
use SilverStripe\CMS\Model\SiteTree;
11
use SilverStripe\Control\Controller;
12
use SilverStripe\Control\Director;
13
use SilverStripe\Core\ClassInfo;
14
use SilverStripe\Core\Injector\Injector;
15
use SilverStripe\Forms\DropdownField;
16
use SilverStripe\Forms\FieldList;
17
use SilverStripe\Forms\HiddenField;
18
use SilverStripe\Forms\NumericField;
19
use SilverStripe\Forms\TextField;
20
use SilverStripe\GraphQL\Scaffolding\StaticSchema;
21
use SilverStripe\ORM\DataObject;
22
use SilverStripe\ORM\FieldType\DBBoolean;
23
use SilverStripe\ORM\FieldType\DBField;
24
use SilverStripe\ORM\FieldType\DBHTMLText;
25
use SilverStripe\Security\Member;
26
use SilverStripe\Security\Permission;
27
use SilverStripe\Versioned\Versioned;
28
use SilverStripe\VersionedAdmin\Forms\HistoryViewerField;
29
use SilverStripe\View\ArrayData;
30
use SilverStripe\View\Parsers\URLSegmentFilter;
31
use SilverStripe\View\Requirements;
32
33
/**
34
 * Class BaseElement
35
 * @package DNADesign\Elemental\Models
36
 *
37
 * @property string $Title
38
 * @property bool $ShowTitle
39
 * @property int $Sort
40
 * @property string $ExtraClass
41
 * @property string $Style
42
 *
43
 * @method ElementalArea Parent()
44
 */
45
class BaseElement extends DataObject
46
{
47
    /**
48
     * Override this on your custom elements to specify a CSS icon class
49
     *
50
     * @var string
51
     */
52
    private static $icon = 'font-icon-block-layout';
0 ignored issues
show
introduced by Robbie Averill
The private property $icon is not used, and could be removed.
Loading history...
53
54
    /**
55
     * Describe the purpose of this element
56
     *
57
     * @config
58
     * @var string
59
     */
60
    private static $description = 'Base element class';
0 ignored issues
show
introduced by Robbie Averill
The private property $description is not used, and could be removed.
Loading history...
61
62
    private static $db = [
0 ignored issues
show
introduced by Will Rossiter
The private property $db is not used, and could be removed.
Loading history...
63
        'Title' => 'Varchar(255)',
64
        'ShowTitle' => 'Boolean',
65
        'Sort' => 'Int',
66
        'ExtraClass' => 'Varchar(255)',
67
        'Style' => 'Varchar(255)'
68
    ];
69
70
    private static $has_one = [
0 ignored issues
show
introduced by Will Rossiter
The private property $has_one is not used, and could be removed.
Loading history...
71
        'Parent' => ElementalArea::class
72
    ];
73
74
    private static $extensions = [
0 ignored issues
show
introduced by Will Rossiter
The private property $extensions is not used, and could be removed.
Loading history...
75
        Versioned::class
76
    ];
77
78
    private static $casting = [
0 ignored issues
show
introduced by Guy Marriott
The private property $casting is not used, and could be removed.
Loading history...
79
        'BlockSchema' => DBObjectType::class,
80
        'IsLiveVersion' => DBBoolean::class,
81
        'IsPublished' => DBBoolean::class,
82
    ];
83
84
    private static $versioned_gridfield_extensions = true;
0 ignored issues
show
introduced by Will Rossiter
The private property $versioned_gridfield_extensions is not used, and could be removed.
Loading history...
85
86
    private static $table_name = 'Element';
0 ignored issues
show
introduced by John Milmine
The private property $table_name is not used, and could be removed.
Loading history...
87
88
    /**
89
     * @var string
90
     */
91
    private static $controller_class = ElementController::class;
92
93
    /**
94
     * @var string
95
     */
96
    private static $controller_template = 'ElementHolder';
0 ignored issues
show
introduced by Will Rossiter
The private property $controller_template is not used, and could be removed.
Loading history...
97
98
    /**
99
     * @var ElementController
100
     */
101
    protected $controller;
102
103
    /**
104
     * Cache various data to improve CMS load time
105
     *
106
     * @internal
107
     * @var array
108
     */
109
    protected $cacheData;
110
111
    private static $default_sort = 'Sort';
0 ignored issues
show
introduced by John Milmine
The private property $default_sort is not used, and could be removed.
Loading history...
112
113
    private static $singular_name = 'block';
0 ignored issues
show
introduced by Robbie Averill
The private property $singular_name is not used, and could be removed.
Loading history...
114
115
    private static $plural_name = 'blocks';
0 ignored issues
show
introduced by Robbie Averill
The private property $plural_name is not used, and could be removed.
Loading history...
116
117
    private static $summary_fields = [
0 ignored issues
show
introduced by Will Rossiter
The private property $summary_fields is not used, and could be removed.
Loading history...
118
        'EditorPreview' => 'Summary'
119
    ];
120
121
    /**
122
     * @config
123
     * @var array
124
     */
125
    private static $styles = [];
0 ignored issues
show
introduced by Will Rossiter
The private property $styles is not used, and could be removed.
Loading history...
126
127
    private static $searchable_fields = [
0 ignored issues
show
introduced by Will Rossiter
The private property $searchable_fields is not used, and could be removed.
Loading history...
128
        'ID' => [
129
            'field' => NumericField::class,
130
        ],
131
        'Title',
132
        'LastEdited'
133
    ];
134
135
    /**
136
     * Enable for backwards compatibility
137
     *
138
     * @var boolean
139
     */
140
    private static $disable_pretty_anchor_name = false;
141
142
    /**
143
     * Set to false to prevent an in-line edit form from showing in an elemental area. Instead the element will be
144
     * clickable and a GridFieldDetailForm will be used.
145
     *
146
     * @config
147
     * @var bool
148
     */
149
    private static $inline_editable = true;
0 ignored issues
show
introduced by Guy Marriott
The private property $inline_editable is not used, and could be removed.
Loading history...
150
151
    /**
152
     * Store used anchor names, this is to avoid title clashes
153
     * when calling 'getAnchor'
154
     *
155
     * @var array
156
     */
157
    protected static $used_anchors = [];
158
159
    /**
160
     * For caching 'getAnchor'
161
     *
162
     * @var string
163
     */
164
    protected $anchor = null;
165
166
    /**
167
     * Basic permissions, defaults to page perms where possible.
168
     *
169
     * @param Member $member
170
     * @return boolean
171
     */
172
    public function canView($member = null)
173
    {
174
        $extended = $this->extendedCan(__FUNCTION__, $member);
175
        if ($extended !== null) {
176
            return $extended;
177
        }
178
179
        if ($this->hasMethod('getPage')) {
180
            if ($page = $this->getPage()) {
181
                return $page->canView($member);
182
            }
183
        }
184
185
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
186
    }
187
188
    /**
189
     * Basic permissions, defaults to page perms where possible.
190
     *
191
     * @param Member $member
192
     *
193
     * @return boolean
194
     */
195
    public function canEdit($member = null)
196
    {
197
        $extended = $this->extendedCan(__FUNCTION__, $member);
198
        if ($extended !== null) {
199
            return $extended;
200
        }
201
202
        if ($this->hasMethod('getPage')) {
203
            if ($page = $this->getPage()) {
204
                return $page->canEdit($member);
205
            }
206
        }
207
208
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
209
    }
210
211
    /**
212
     * Basic permissions, defaults to page perms where possible.
213
     *
214
     * Uses archive not delete so that current stage is respected i.e if a
215
     * element is not published, then it can be deleted by someone who doesn't
216
     * have publishing permissions.
217
     *
218
     * @param Member $member
219
     *
220
     * @return boolean
221
     */
222
    public function canDelete($member = null)
223
    {
224
        $extended = $this->extendedCan(__FUNCTION__, $member);
225
        if ($extended !== null) {
226
            return $extended;
227
        }
228
229
        if ($this->hasMethod('getPage')) {
230
            if ($page = $this->getPage()) {
231
                return $page->canArchive($member);
232
            }
233
        }
234
235
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
236
    }
237
238
    /**
239
     * Basic permissions, defaults to page perms where possible.
240
     *
241
     * @param Member $member
242
     * @param array $context
243
     *
244
     * @return boolean
245
     */
246
    public function canCreate($member = null, $context = array())
247
    {
248
        $extended = $this->extendedCan(__FUNCTION__, $member);
249
        if ($extended !== null) {
250
            return $extended;
251
        }
252
253
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
254
    }
255
256
    /**
257
     * Increment the sort order if one hasn't been already defined. This
258
     * ensures that new elements are created at the end of the list by default.
259
     *
260
     * {@inheritDoc}
261
     */
262
    public function onBeforeWrite()
263
    {
264
        parent::onBeforeWrite();
265
266
        if (!$this->Sort) {
267
            if ($this->hasExtension(Versioned::class)) {
268
                $records = Versioned::get_by_stage(BaseElement::class, Versioned::DRAFT);
269
            } else {
270
                $records = BaseElement::get();
271
            }
272
273
            $records = $records->filter('ParentID', $this->ParentID);
0 ignored issues
show
Bug Best Practice introduced by Will Rossiter
The property ParentID does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __get, consider adding a @property annotation.
Loading history...
274
275
            $this->Sort = $records->max('Sort') + 1;
276
        }
277
    }
278
279
    public function getCMSFields()
280
    {
281
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
282
            // Remove relationship fields
283
            $fields->removeByName('ParentID');
284
            $fields->removeByName('Sort');
285
286
            // Remove link and file tracking tabs
287
            $fields->removeByName(['LinkTracking', 'FileTracking']);
288
289
            $fields->addFieldToTab(
290
                'Root.Settings',
291
                TextField::create('ExtraClass', _t(__CLASS__ . '.ExtraCssClassesLabel', 'Custom CSS classes'))
292
                    ->setAttribute(
293
                        'placeholder',
294
                        _t(__CLASS__ . '.ExtraCssClassesPlaceholder', 'my_class another_class')
295
                    )
296
            );
297
298
            // Rename the "Settings" tab
299
            $fields->fieldByName('Root.Settings')
300
                ->setTitle(_t(__CLASS__ . '.SettingsTabLabel', 'Settings'));
301
302
            // Add a combined field for "Title" and "Displayed" checkbox in a Bootstrap input group
303
            $fields->removeByName('ShowTitle');
304
            $fields->replaceField(
305
                'Title',
306
                TextCheckboxGroupField::create()
307
                    ->setName('Title')
308
            );
309
310
            // Rename the "Main" tab
311
            $fields->fieldByName('Root.Main')
312
                ->setTitle(_t(__CLASS__ . '.MainTabLabel', 'Content'));
313
314
            $fields->addFieldsToTab('Root.Main', [
315
                HiddenField::create('AbsoluteLink', false, Director::absoluteURL($this->PreviewLink())),
316
                HiddenField::create('LiveLink', false, Director::absoluteURL($this->Link())),
317
                HiddenField::create('StageLink', false, Director::absoluteURL($this->PreviewLink())),
318
            ]);
319
320
            $styles = $this->config()->get('styles');
321
322
            if ($styles && count($styles) > 0) {
323
                $styleDropdown = DropdownField::create('Style', _t(__CLASS__.'.STYLE', 'Style variation'), $styles);
324
325
                $fields->insertBefore($styleDropdown, 'ExtraClass');
0 ignored issues
show
Bug introduced by Will Rossiter
'ExtraClass' of type string is incompatible with the type SilverStripe\Forms\FormField expected by parameter $item of SilverStripe\Forms\FieldList::insertBefore(). ( Ignorable by Annotation )

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

325
                $fields->insertBefore($styleDropdown, /** @scrutinizer ignore-type */ 'ExtraClass');
Loading history...
326
327
                $styleDropdown->setEmptyString(_t(__CLASS__.'.CUSTOM_STYLES', 'Select a style..'));
328
            } else {
329
                $fields->removeByName('Style');
330
            }
331
332
            // Hide the navigation section of the tabs in the React component {@see silverstripe/admin Tabs}
333
            $rootTabset = $fields->fieldByName('Root');
334
            $rootTabset->setSchemaState(['hideNav' => true]);
335
336
            if ($this->isInDB()) {
337
                $fields->addFieldsToTab('Root.History', [
338
                    HistoryViewerField::create('ElementHistory'),
339
                ]);
340
                // Add class to containing tab
341
                $fields->fieldByName('Root.History')
342
                    ->addExtraClass('elemental-block__history-tab tab--history-viewer');
343
344
                // Hack: automatically navigate to the History tab with `#Root_History` is in the URL
345
                // To unhack, fix this: https://github.com/silverstripe/silverstripe-admin/issues/911
346
                Requirements::customScript(<<<JS
347
    document.addEventListener('DOMContentLoaded', () => {
348
        var hash = window.location.hash.substr(1);
349
        if (hash !== 'Root_History') {
350
            return null;
351
        }
352
        jQuery('.cms-tabset-nav-primary li[aria-controls="Root_History"] a').trigger('click')
353
    });
354
JS
355
                );
356
            }
357
        });
358
359
        return parent::getCMSFields();
360
    }
361
362
    /**
363
     * Get the type of the current block, for use in GridField summaries, block
364
     * type dropdowns etc. Examples are "Content", "File", "Media", etc.
365
     *
366
     * @return string
367
     */
368
    public function getType()
369
    {
370
        return _t(__CLASS__ . '.BlockType', 'Block');
371
    }
372
373
    /**
374
     * Proxy through to configuration setting 'inline_editable'
375
     *
376
     * @return bool
377
     */
378
    public function inlineEditable()
379
    {
380
        return static::config()->get('inline_editable');
381
    }
382
383
    /**
384
     * @param ElementController $controller
385
     *
386
     * @return $this
387
     */
388
    public function setController($controller)
389
    {
390
        $this->controller = $controller;
391
392
        return $this;
393
    }
394
395
    /**
396
     * @throws Exception If the specified controller class doesn't exist
397
     *
398
     * @return ElementController
399
     */
400
    public function getController()
401
    {
402
        if ($this->controller) {
403
            return $this->controller;
404
        }
405
406
        $controllerClass = self::config()->controller_class;
407
408
        if (!class_exists($controllerClass)) {
409
            throw new Exception(
410
                'Could not find controller class ' . $controllerClass . ' as defined in ' . static::class
411
            );
412
        }
413
414
        $this->controller = Injector::inst()->create($controllerClass, $this);
415
        $this->controller->doInit();
416
417
        return $this->controller;
418
    }
419
420
    /**
421
     * @param string $name
422
     * @return $this
423
     */
424
    public function setAreaRelationNameCache($name)
425
    {
426
        $this->cacheData['area_relation_name'] = $name;
427
428
        return $this;
429
    }
430
431
    /**
432
     * @return Controller
433
     */
434
    public function Top()
435
    {
436
        return (Controller::has_curr()) ? Controller::curr() : null;
437
    }
438
439
    /**
440
     * Default way to render element in templates. Note that all blocks should
441
     * be rendered through their {@link ElementController} class as this
442
     * contains the holder styles.
443
     *
444
     * @return string|null HTML
445
     */
446
    public function forTemplate($holder = true)
447
    {
448
        $templates = $this->getRenderTemplates();
449
450
        if ($templates) {
451
            return $this->renderWith($templates);
452
        }
453
454
        return null;
455
    }
456
457
    /**
458
     * @param string $suffix
459
     *
460
     * @return array
461
     */
462
    public function getRenderTemplates($suffix = '')
463
    {
464
        $classes = ClassInfo::ancestry($this->ClassName);
465
        $classes[static::class] = static::class;
466
        $classes = array_reverse($classes);
467
        $templates = [];
468
469
        foreach ($classes as $key => $class) {
470
            if ($class == BaseElement::class) {
471
                continue;
472
            }
473
474
            if ($class == DataObject::class) {
475
                break;
476
            }
477
478
            if ($style = $this->Style) {
479
                $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName() . '_' . $style;
480
                $templates[$class][] = $class . $suffix . '_' . $style;
481
            }
482
            $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName();
483
            $templates[$class][] = $class . $suffix;
484
        }
485
486
        $this->extend('updateRenderTemplates', $templates, $suffix);
487
488
        $templateFlat = [];
489
        foreach ($templates as $class => $variations) {
490
            $templateFlat = array_merge($templateFlat, $variations);
491
        }
492
493
        return $templateFlat;
494
    }
495
496
    /**
497
     * Given form data (wit
498
     *
499
     * @param $data
500
     */
501
    public function updateFromFormData($data)
502
    {
503
        $cmsFields = $this->getCMSFields();
504
505
        foreach ($data as $field => $datum) {
506
            $field = $cmsFields->dataFieldByName($field);
507
508
            if (!$field) {
509
                continue;
510
            }
511
512
            $field->setSubmittedValue($datum);
513
            $field->saveInto($this);
514
        }
515
    }
516
517
    /**
518
     * Strip all namespaces from class namespace.
519
     *
520
     * @param string $classname e.g. "\Fully\Namespaced\Class"
521
     *
522
     * @return string following the param example, "Class"
523
     */
524
    protected function stripNamespacing($classname)
525
    {
526
        $classParts = explode('\\', $classname);
527
        return array_pop($classParts);
528
    }
529
530
    /**
531
     * @return string
532
     */
533
    public function getSimpleClassName()
534
    {
535
        return strtolower($this->sanitiseClassName($this->ClassName, '__'));
536
    }
537
538
    /**
539
     * @return null|SiteTree
540
     * @throws \Psr\Container\NotFoundExceptionInterface
541
     * @throws \SilverStripe\ORM\ValidationException
542
     */
543
    public function getPage()
544
    {
545
        // Allow for repeated calls to be cached
546
        if (isset($this->cacheData['page'])) {
547
            return $this->cacheData['page'];
548
        }
549
550
551
        $class = DataObject::getSchema()->hasOneComponent($this, 'Parent');
552
        $area = ($this->ParentID) ? DataObject::get_by_id($class, $this->ParentID) : null;
0 ignored issues
show
Bug Best Practice introduced by Loz Calver
The property ParentID does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __get, consider adding a @property annotation.
Loading history...
553
        if ($area instanceof ElementalArea && $area->exists()) {
554
            $this->cacheData['page'] = $area->getOwnerPage();
555
            return $this->cacheData['page'];
556
        }
557
558
        return null;
559
    }
560
561
    /**
562
     * Get a unique anchor name
563
     *
564
     * @return string
565
     */
566
    public function getAnchor()
567
    {
568
        if ($this->anchor !== null) {
569
            return $this->anchor;
570
        }
571
572
        $anchorTitle = '';
573
574
        if (!$this->config()->disable_pretty_anchor_name) {
575
            if ($this->hasMethod('getAnchorTitle')) {
576
                $anchorTitle = $this->getAnchorTitle();
0 ignored issues
show
Bug introduced by John Milmine
The method getAnchorTitle() does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

576
                /** @scrutinizer ignore-call */ 
577
                $anchorTitle = $this->getAnchorTitle();
Loading history...
577
            } elseif ($this->config()->enable_title_in_template) {
578
                $anchorTitle = $this->getField('Title');
579
            }
580
        }
581
582
        if (!$anchorTitle) {
583
            $anchorTitle = 'e'.$this->ID;
584
        }
585
586
        $filter = URLSegmentFilter::create();
587
        $titleAsURL = $filter->filter($anchorTitle);
588
589
        // Ensure that this anchor name isn't already in use
590
        // ie. If two elemental blocks have the same title, it'll append '-2', '-3'
591
        $result = $titleAsURL;
592
        $count = 1;
593
        while (isset(self::$used_anchors[$result]) && self::$used_anchors[$result] !== $this->ID) {
594
            ++$count;
595
            $result = $titleAsURL . '-' . $count;
596
        }
597
        self::$used_anchors[$result] = $this->ID;
598
        return $this->anchor = $result;
599
    }
600
601
    /**
602
     * @param string|null $action
603
     * @return string|null
604
     * @throws \Psr\Container\NotFoundExceptionInterface
605
     * @throws \SilverStripe\ORM\ValidationException
606
     */
607
    public function AbsoluteLink($action = null)
608
    {
609
        if ($page = $this->getPage()) {
610
            $link = $page->AbsoluteLink($action) . '#' . $this->getAnchor();
611
612
            return $link;
613
        }
614
615
        return null;
616
    }
617
618
    /**
619
     * @param string|null $action
620
     * @return string
621
     * @throws \Psr\Container\NotFoundExceptionInterface
622
     * @throws \SilverStripe\ORM\ValidationException
623
     */
624
    public function Link($action = null)
625
    {
626
        if ($page = $this->getPage()) {
627
            $link = $page->Link($action) . '#' . $this->getAnchor();
628
629
            $this->extend('updateLink', $link);
630
631
            return $link;
632
        }
633
634
        return null;
635
    }
636
637
    /**
638
     * @param string|null $action
639
     * @return string
640
     * @throws \Psr\Container\NotFoundExceptionInterface
641
     * @throws \SilverStripe\ORM\ValidationException
642
     */
643
    public function PreviewLink($action = null)
644
    {
645
        $action = $action . '?ElementalPreview=' . mt_rand();
646
        $link = $this->Link($action);
647
        $this->extend('updatePreviewLink', $link);
648
649
        return $link;
650
    }
651
652
    /**
653
     * @return boolean
654
     */
655
    public function isCMSPreview()
656
    {
657
        if (Controller::has_curr()) {
658
            $controller = Controller::curr();
659
660
            if ($controller->getRequest()->requestVar('CMSPreview')) {
661
                return true;
662
            }
663
        }
664
665
        return false;
666
    }
667
668
    /**
669
     * @param bool $directLink Indicates that the GridFieldDetailEdit form link should be given even if the block can be
670
     *                         edited in-line.
671
     * @return null|string
672
     * @throws \SilverStripe\ORM\ValidationException
673
     */
674
    public function CMSEditLink($directLink = false)
675
    {
676
        // Allow for repeated calls to be returned from cache
677
        if (isset($this->cacheData['cms_edit_link'])) {
678
            return $this->cacheData['cms_edit_link'];
679
        }
680
681
        $relationName = $this->getAreaRelationName();
682
        $page = $this->getPage();
683
684
        if (!$page) {
685
            return null;
686
        }
687
688
        if (!$page instanceof SiteTree && method_exists($page, 'CMSEditLink')) {
0 ignored issues
show
introduced by Corey Sewell
$page is always a sub-type of SilverStripe\CMS\Model\SiteTree.
Loading history...
689
            $link = Controller::join_links($page->CMSEditLink(), 'ItemEditForm');
690
        } else {
691
            $link = $page->CMSEditLink();
692
        }
693
694
        // In-line editable blocks should just take you to the page. Editable ones should add the suffix for detail form
695
        if (!$this->inlineEditable() || $directLink) {
696
            $link = Controller::join_links(
697
                singleton(CMSPageEditController::class)->Link('EditForm'),
698
                $page->ID,
699
                'field/' . $relationName . '/item/',
700
                $this->ID,
701
                'edit'
702
            );
703
        }
704
705
        $this->extend('updateCMSEditLink', $link);
706
707
        $this->cacheData['cms_edit_link'] = $link;
708
        return $link;
709
    }
710
711
    /**
712
     * Retrieve a elemental area relation for creating cms links
713
     *
714
     * @return int|string The name of a valid elemental area relation
715
     * @throws \Psr\Container\NotFoundExceptionInterface
716
     * @throws \SilverStripe\ORM\ValidationException
717
     */
718
    public function getAreaRelationName()
719
    {
720
        // Allow repeated calls to return from internal cache
721
        if (isset($this->cacheData['area_relation_name'])) {
722
            return $this->cacheData['area_relation_name'];
723
        }
724
725
        $page = $this->getPage();
726
727
        $result = 'ElementalArea';
728
729
        if ($page) {
730
            $has_one = $page->config()->get('has_one');
731
            $area = $this->Parent();
732
733
            foreach ($has_one as $relationName => $relationClass) {
734
                if ($page instanceof BaseElement && $relationName === 'Parent') {
735
                    continue;
736
                }
737
                if ($relationClass === $area->ClassName && $page->{$relationName}()->ID === $area->ID) {
738
                    $result = $relationName;
739
                    break;
740
                }
741
            }
742
        }
743
744
        $this->setAreaRelationNameCache($result);
745
746
        return $result;
747
    }
748
749
    /**
750
     * Sanitise a model class' name for inclusion in a link.
751
     *
752
     * @return string
753
     */
754
    public function sanitiseClassName($class, $delimiter = '-')
755
    {
756
        return str_replace('\\', $delimiter, $class);
757
    }
758
759
    public function unsanitiseClassName($class, $delimiter = '-')
760
    {
761
        return str_replace($delimiter, '\\', $class);
762
    }
763
764
    /**
765
     * @return null|string
766
     * @throws \Psr\Container\NotFoundExceptionInterface
767
     * @throws \SilverStripe\ORM\ValidationException
768
     */
769
    public function getEditLink()
770
    {
771
        return Director::absoluteURL($this->CMSEditLink());
772
    }
773
774
    /**
775
     * @return DBField|null
776
     * @throws \Psr\Container\NotFoundExceptionInterface
777
     * @throws \SilverStripe\ORM\ValidationException
778
     */
779
    public function PageCMSEditLink()
780
    {
781
        if ($page = $this->getPage()) {
782
            return DBField::create_field('HTMLText', sprintf(
783
                '<a href="%s">%s</a>',
784
                $page->CMSEditLink(),
785
                $page->Title
786
            ));
787
        }
788
789
        return null;
790
    }
791
792
    /**
793
     * @return string
794
     */
795
    public function getMimeType()
796
    {
797
        return 'text/html';
798
    }
799
800
    /**
801
     * This can be overridden on child elements to create a summary for display
802
     * in GridFields.
803
     *
804
     * @return string
805
     */
806
    public function getSummary()
807
    {
808
        return '';
809
    }
810
811
    /**
812
     * The block config defines a set of data (usually set through config on the element) that will be made available in
813
     * client side config. Individual element types may choose to add config variable for use in React code
814
     *
815
     * @return array
816
     */
817
    public static function getBlockConfig()
818
    {
819
        return [];
820
    }
821
822
    /**
823
     * The block actions is an associative array available for providing data to the client side to be used to describe
824
     * actions that may be performed. This is available as a plain "ObjectType" in the GraphQL schema.
825
     *
826
     * By default the only action is "edit" which is simply the URL where the block may be edited.
827
     *
828
     * To modify the actions, either use the extension point or overload the `provideBlockSchema` method.
829
     *
830
     * @internal This API may change in future. Treat this as a `final` method.
831
     * @return array
832
     */
833
    public function getBlockSchema()
834
    {
835
        $blockSchema = $this->provideBlockSchema();
836
837
        $this->extend('updateBlockSchema', $blockSchema);
838
839
        return $blockSchema;
840
    }
841
842
    /**
843
     * Provide block schema data, which will be serialised and sent via GraphQL to the editor client.
844
     *
845
     * Overload this method in child element classes to augment, or use the extension point on `getBlockSchema`
846
     * to update it from an `Extension`.
847
     *
848
     * @return array
849
     */
850
    protected function provideBlockSchema()
851
    {
852
        return [
853
            // Currently GraphQL doesn't expose the correct type name and just returns "base element"s. This is a
854
            // workaround until we can scaffold a query client side that specifies by type name
855
            'typeName' => StaticSchema::inst()->typeNameForDataObject(static::class),
856
            'actions' => [
857
                'edit' => $this->getEditLink(),
858
            ],
859
        ];
860
    }
861
862
    /**
863
     * Generate markup for element type icons suitable for use in GridFields.
864
     *
865
     * @return null|DBHTMLText
866
     */
867
    public function getIcon()
868
    {
869
        $data = ArrayData::create([]);
870
871
        $iconClass = $this->config()->get('icon');
872
        if ($iconClass) {
873
            $data->IconClass = $iconClass;
874
875
            // Add versioned states (rendered as a circle over the icon)
876
            if ($this->hasExtension(Versioned::class)) {
877
                $data->IsVersioned = true;
878
                if ($this->isOnDraftOnly()) {
0 ignored issues
show
Bug introduced by Robbie Averill
The method isOnDraftOnly() does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

878
                if ($this->/** @scrutinizer ignore-call */ isOnDraftOnly()) {
Loading history...
879
                    $data->VersionState = 'draft';
880
                    $data->VersionStateTitle = _t(
881
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.ADDEDTODRAFTHELP',
882
                        'Item has not been published yet'
883
                    );
884
                } elseif ($this->isModifiedOnDraft()) {
0 ignored issues
show
Bug introduced by Robbie Averill
The method isModifiedOnDraft() does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

884
                } elseif ($this->/** @scrutinizer ignore-call */ isModifiedOnDraft()) {
Loading history...
885
                    $data->VersionState = 'modified';
886
                    $data->VersionStateTitle = $data->VersionStateTitle = _t(
887
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.MODIFIEDONDRAFTHELP',
888
                        'Item has unpublished changes'
889
                    );
890
                }
891
            }
892
893
            return $data->renderWith(__CLASS__ . '/PreviewIcon');
894
        }
895
896
        return null;
897
    }
898
899
    /**
900
     * Get a description for this content element, if available
901
     *
902
     * @return string
903
     */
904
    public function getDescription()
905
    {
906
        $description = $this->config()->uninherited('description');
907
        if ($description) {
908
            return _t(__CLASS__ . '.Description', $description);
909
        }
910
        return '';
911
    }
912
913
    /**
914
     * Generate markup for element type, with description suitable for use in
915
     * GridFields.
916
     *
917
     * @return DBField
918
     */
919
    public function getTypeNice()
920
    {
921
        $description = $this->getDescription();
922
        $desc = ($description) ? ' <span class="element__note"> &mdash; ' . $description . '</span>' : '';
923
924
        return DBField::create_field(
925
            'HTMLVarchar',
926
            $this->getType() . $desc
927
        );
928
    }
929
930
    /**
931
     * @return \SilverStripe\ORM\FieldType\DBHTMLText
932
     */
933
    public function getEditorPreview()
934
    {
935
        $templates = $this->getRenderTemplates('_EditorPreview');
936
        $templates[] = BaseElement::class . '_EditorPreview';
937
938
        return $this->renderWith($templates);
939
    }
940
941
    /**
942
     * @return Member
943
     */
944
    public function getAuthor()
945
    {
946
        if ($this->AuthorID) {
0 ignored issues
show
Bug Best Practice introduced by Will Rossiter
The property AuthorID does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __get, consider adding a @property annotation.
Loading history...
947
            return Member::get()->byId($this->AuthorID);
948
        }
949
950
        return null;
951
    }
952
953
    /**
954
     * Get a user defined style variant for this element, if available
955
     *
956
     * @return string
957
     */
958
    public function getStyleVariant()
959
    {
960
        $style = $this->Style;
961
        $styles = $this->config()->get('styles');
962
963
        if (isset($styles[$style])) {
964
            $style = strtolower($style);
965
        } else {
966
            $style = '';
967
        }
968
969
        $this->extend('updateStyleVariant', $style);
970
971
        return $style;
972
    }
973
974
    /**
975
     * @return mixed|null
976
     * @throws \Psr\Container\NotFoundExceptionInterface
977
     * @throws \SilverStripe\ORM\ValidationException
978
     */
979
    public function getPageTitle()
980
    {
981
        $page = $this->getPage();
982
983
        if ($page) {
984
            return $page->Title;
985
        }
986
987
        return null;
988
    }
989
990
    /**
991
     * @return boolean
992
     */
993
    public function First()
994
    {
995
        return ($this->Parent()->Elements()->first()->ID === $this->ID);
996
    }
997
998
    /**
999
     * @return boolean
1000
     */
1001
    public function Last()
1002
    {
1003
        return ($this->Parent()->Elements()->last()->ID === $this->ID);
1004
    }
1005
1006
    /**
1007
     * @return int
1008
     */
1009
    public function TotalItems()
1010
    {
1011
        return $this->Parent()->Elements()->count();
1012
    }
1013
1014
    /**
1015
     * Returns the position of the current element.
1016
     *
1017
     * @return int
1018