Passed
Push — master ( ecccb9...b91912 )
by Robbie
08:46
created

BaseElement::CMSEditLink()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 38
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 22
nc 6
nop 0
dl 0
loc 38
rs 8.9457
c 0
b 0
f 0
1
<?php
2
3
namespace DNADesign\Elemental\Models;
4
5
use DNADesign\Elemental\Controllers\ElementController;
6
use DNADesign\Elemental\Forms\EditFormFactory;
7
use DNADesign\Elemental\Forms\TextCheckboxGroupField;
8
use DNADesign\Elemental\ORM\FieldType\DBObjectType;
9
use Exception;
10
use SilverStripe\CMS\Controllers\CMSPageEditController;
11
use SilverStripe\CMS\Model\SiteTree;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Core\ClassInfo;
15
use SilverStripe\Core\Injector\Injector;
16
use SilverStripe\Forms\CheckboxField;
17
use SilverStripe\Forms\DropdownField;
18
use SilverStripe\Forms\FieldList;
19
use SilverStripe\Forms\HiddenField;
20
use SilverStripe\Forms\NumericField;
21
use SilverStripe\Forms\TextField;
22
use SilverStripe\GraphQL\Scaffolding\StaticSchema;
23
use SilverStripe\ORM\DataObject;
24
use SilverStripe\ORM\FieldType\DBBoolean;
25
use SilverStripe\ORM\FieldType\DBField;
26
use SilverStripe\ORM\FieldType\DBHTMLText;
27
use SilverStripe\Security\Member;
28
use SilverStripe\Security\Permission;
29
use SilverStripe\Versioned\Versioned;
30
use SilverStripe\View\ArrayData;
31
use SilverStripe\View\Parsers\URLSegmentFilter;
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
            // Add a combined field for "Title" and "Displayed" checkbox in a Bootstrap input group
299
            $fields->removeByName('ShowTitle');
300
            $fields->replaceField(
301
                'Title',
302
                TextCheckboxGroupField::create()
303
                    ->setName('Title')
304
            );
305
306
            // Rename the "Main" tab
307
            $fields->fieldByName('Root.Main')
308
                ->setTitle(_t(__CLASS__ . '.MainTabLabel', 'Content'));
309
310
            $fields->addFieldsToTab('Root.Main', [
311
                HiddenField::create('AbsoluteLink', false, Director::absoluteURL($this->PreviewLink())),
312
                HiddenField::create('LiveLink', false, Director::absoluteURL($this->Link())),
313
                HiddenField::create('StageLink', false, Director::absoluteURL($this->PreviewLink())),
314
            ]);
315
316
            $styles = $this->config()->get('styles');
317
318
            if ($styles && count($styles) > 0) {
319
                $styleDropdown = DropdownField::create('Style', _t(__CLASS__.'.STYLE', 'Style variation'), $styles);
320
321
                $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

321
                $fields->insertBefore($styleDropdown, /** @scrutinizer ignore-type */ 'ExtraClass');
Loading history...
322
323
                $styleDropdown->setEmptyString(_t(__CLASS__.'.CUSTOM_STYLES', 'Select a style..'));
324
            } else {
325
                $fields->removeByName('Style');
326
            }
327
328
            // Hide the navigation section of the tabs in the React component {@see silverstripe/admin Tabs}
329
            $rootTabset = $fields->fieldByName('Root');
330
            $rootTabset->setSchemaState(['hideNav' => true]);
331
        });
332
333
        return parent::getCMSFields();
334
    }
335
336
    /**
337
     * Get the type of the current block, for use in GridField summaries, block
338
     * type dropdowns etc. Examples are "Content", "File", "Media", etc.
339
     *
340
     * @return string
341
     */
342
    public function getType()
343
    {
344
        return _t(__CLASS__ . '.BlockType', 'Block');
345
    }
346
347
    /**
348
     * Proxy through to configuration setting 'inline_editable'
349
     *
350
     * @return bool
351
     */
352
    public function inlineEditable()
353
    {
354
        return static::config()->get('inline_editable');
355
    }
356
357
    /**
358
     * @param ElementController $controller
359
     *
360
     * @return $this
361
     */
362
    public function setController($controller)
363
    {
364
        $this->controller = $controller;
365
366
        return $this;
367
    }
368
369
    /**
370
     * @throws Exception If the specified controller class doesn't exist
371
     *
372
     * @return ElementController
373
     */
374
    public function getController()
375
    {
376
        if ($this->controller) {
377
            return $this->controller;
378
        }
379
380
        $controllerClass = self::config()->controller_class;
381
382
        if (!class_exists($controllerClass)) {
383
            throw new Exception(
384
                'Could not find controller class ' . $controllerClass . ' as defined in ' . static::class
385
            );
386
        }
387
388
        $this->controller = Injector::inst()->create($controllerClass, $this);
389
        $this->controller->doInit();
390
391
        return $this->controller;
392
    }
393
394
    /**
395
     * @param string $name
396
     * @return $this
397
     */
398
    public function setAreaRelationNameCache($name)
399
    {
400
        $this->cacheData['area_relation_name'] = $name;
401
402
        return $this;
403
    }
404
405
    /**
406
     * @return Controller
407
     */
408
    public function Top()
409
    {
410
        return (Controller::has_curr()) ? Controller::curr() : null;
411
    }
412
413
    /**
414
     * Default way to render element in templates. Note that all blocks should
415
     * be rendered through their {@link ElementController} class as this
416
     * contains the holder styles.
417
     *
418
     * @return string|null HTML
419
     */
420
    public function forTemplate($holder = true)
421
    {
422
        $templates = $this->getRenderTemplates();
423
424
        if ($templates) {
425
            return $this->renderWith($templates);
426
        }
427
428
        return null;
429
    }
430
431
    /**
432
     * @param string $suffix
433
     *
434
     * @return array
435
     */
436
    public function getRenderTemplates($suffix = '')
437
    {
438
        $classes = ClassInfo::ancestry($this->ClassName);
439
        $classes[static::class] = static::class;
440
        $classes = array_reverse($classes);
441
        $templates = [];
442
443
        foreach ($classes as $key => $class) {
444
            if ($class == BaseElement::class) {
445
                continue;
446
            }
447
448
            if ($class == DataObject::class) {
449
                break;
450
            }
451
452
            if ($style = $this->Style) {
453
                $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName() . '_' . $style;
454
                $templates[$class][] = $class . $suffix . '_' . $style;
455
            }
456
            $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName();
457
            $templates[$class][] = $class . $suffix;
458
        }
459
460
        $this->extend('updateRenderTemplates', $templates, $suffix);
461
462
        $templateFlat = [];
463
        foreach ($templates as $class => $variations) {
464
            $templateFlat = array_merge($templateFlat, $variations);
465
        }
466
467
        return $templateFlat;
468
    }
469
470
    /**
471
     * Given form data (wit
472
     *
473
     * @param $data
474
     */
475
    public function updateFromFormData($data)
476
    {
477
        $cmsFields = $this->getCMSFields();
478
479
        foreach ($data as $field => $datum) {
480
            $field = $cmsFields->dataFieldByName($field);
481
482
            if (!$field) {
483
                continue;
484
            }
485
486
            $field->setSubmittedValue($datum);
487
            $field->saveInto($this);
488
        }
489
    }
490
491
    /**
492
     * Strip all namespaces from class namespace.
493
     *
494
     * @param string $classname e.g. "\Fully\Namespaced\Class"
495
     *
496
     * @return string following the param example, "Class"
497
     */
498
    protected function stripNamespacing($classname)
499
    {
500
        $classParts = explode('\\', $classname);
501
        return array_pop($classParts);
502
    }
503
504
    /**
505
     * @return string
506
     */
507
    public function getSimpleClassName()
508
    {
509
        return strtolower($this->sanitiseClassName($this->ClassName, '__'));
510
    }
511
512
    /**
513
     * @return null|SiteTree
514
     * @throws \Psr\Container\NotFoundExceptionInterface
515
     * @throws \SilverStripe\ORM\ValidationException
516
     */
517
    public function getPage()
518
    {
519
        // Allow for repeated calls to be cached
520
        if (isset($this->cacheData['page'])) {
521
            return $this->cacheData['page'];
522
        }
523
524
        $area = $this->Parent();
525
526
        if ($area instanceof ElementalArea && $area->exists()) {
527
            $this->cacheData['page'] = $area->getOwnerPage();
528
            return $this->cacheData['page'];
529
        }
530
531
        return null;
532
    }
533
534
    /**
535
     * Get a unique anchor name
536
     *
537
     * @return string
538
     */
539
    public function getAnchor()
540
    {
541
        if ($this->anchor !== null) {
542
            return $this->anchor;
543
        }
544
545
        $anchorTitle = '';
546
547
        if (!$this->config()->disable_pretty_anchor_name) {
548
            if ($this->hasMethod('getAnchorTitle')) {
549
                $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

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

853
                if ($this->/** @scrutinizer ignore-call */ isOnDraftOnly()) {
Loading history...
854
                    $data->VersionState = 'draft';
855
                    $data->VersionStateTitle = _t(
856
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.ADDEDTODRAFTHELP',
857
                        'Item has not been published yet'
858
                    );
859
                } 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

859
                } elseif ($this->/** @scrutinizer ignore-call */ isModifiedOnDraft()) {
Loading history...
860
                    $data->VersionState = 'modified';
861
                    $data->VersionStateTitle = $data->VersionStateTitle = _t(
862
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.MODIFIEDONDRAFTHELP',
863
                        'Item has unpublished changes'
864
                    );
865
                }
866
            }
867
868
            return $data->renderWith(__CLASS__ . '/PreviewIcon');
869
        }
870
871
        return null;
872
    }
873
874
    /**
875
     * Get a description for this content element, if available
876
     *
877
     * @return string
878
     */
879
    public function getDescription()
880
    {
881
        $description = $this->config()->uninherited('description');
882
        if ($description) {
883
            return _t(__CLASS__ . '.Description', $description);
884
        }
885
        return '';
886
    }
887
888
    /**
889
     * Generate markup for element type, with description suitable for use in
890
     * GridFields.
891
     *
892
     * @return DBField
893
     */
894
    public function getTypeNice()
895
    {
896
        $description = $this->getDescription();
897
        $desc = ($description) ? ' <span class="element__note"> &mdash; ' . $description . '</span>' : '';
898
899
        return DBField::create_field(
900
            'HTMLVarchar',
901
            $this->getType() . $desc
902
        );
903
    }
904
905
    /**
906
     * @return \SilverStripe\ORM\FieldType\DBHTMLText
907
     */
908
    public function getEditorPreview()
909
    {
910
        $templates = $this->getRenderTemplates('_EditorPreview');
911
        $templates[] = BaseElement::class . '_EditorPreview';
912
913
        return $this->renderWith($templates);
914
    }
915
916
    /**
917
     * @return Member
918
     */
919
    public function getAuthor()
920
    {
921
        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...
922
            return Member::get()->byId($this->AuthorID);
923
        }
924
925
        return null;
926
    }
927
928
    /**
929
     * Get a user defined style variant for this element, if available
930
     *
931
     * @return string
932
     */
933
    public function getStyleVariant()
934
    {
935
        $style = $this->Style;
936
        $styles = $this->config()->get('styles');
937
938
        if (isset($styles[$style])) {
939
            $style = strtolower($style);
940
        } else {
941
            $style = '';
942
        }
943
944
        $this->extend('updateStyleVariant', $style);
945
946
        return $style;
947
    }
948
949
    /**
950
     * @return mixed|null
951
     * @throws \Psr\Container\NotFoundExceptionInterface
952
     * @throws \SilverStripe\ORM\ValidationException
953
     */
954
    public function getPageTitle()
955
    {
956
        $page = $this->getPage();
957
958
        if ($page) {
959
            return $page->Title;
960
        }
961
962
        return null;
963
    }
964
965
    /**
966
     * @return boolean
967
     */
968
    public function First()
969
    {
970
        return ($this->Parent()->Elements()->first()->ID === $this->ID);
971
    }
972
973
    /**
974
     * @return boolean
975
     */
976
    public function Last()
977
    {
978
        return ($this->Parent()->Elements()->last()->ID === $this->ID);
979
    }
980
981
    /**
982
     * @return int
983
     */
984
    public function TotalItems()
985
    {
986
        return $this->Parent()->Elements()->count();
987
    }
988
989
    /**
990
     * Returns the position of the current element.
991
     *
992
     * @return int
993
     */
994
    public function Pos()
995
    {
996
        return ($this->Parent()->Elements()->filter('Sort:LessThan', $this->Sort)->count() + 1);
997
    }
998
999
    /**
1000
     * @return string
1001
     */
1002
    public function EvenOdd()
1003
    {
1004
        $odd = (bool) ($this->Pos() % 2);
1005
1006
        return  ($odd) ? 'odd' : 'even';
1007
    }
1008
}
1009