Passed
Push — master ( b91912...ff5896 )
by Robbie
07:30 queued 10s
created

src/Models/BaseElement.php (8 issues)

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
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
The private property $description is not used, and could be removed.
Loading history...
61
62
    private static $db = [
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 = [
71
        'Parent' => ElementalArea::class
72
    ];
73
74
    private static $extensions = [
75
        Versioned::class
76
    ];
77
78
    private static $casting = [
0 ignored issues
show
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;
85
86
    private static $table_name = 'Element';
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';
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';
112
113
    private static $singular_name = 'block';
0 ignored issues
show
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
The private property $plural_name is not used, and could be removed.
Loading history...
116
117
    private static $summary_fields = [
118
        'EditorPreview' => 'Summary'
119
    ];
120
121
    /**
122
     * @config
123
     * @var array
124
     */
125
    private static $styles = [];
126
127
    private static $searchable_fields = [
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
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);
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');
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
337
        return parent::getCMSFields();
338
    }
339
340
    /**
341
     * Get the type of the current block, for use in GridField summaries, block
342
     * type dropdowns etc. Examples are "Content", "File", "Media", etc.
343
     *
344
     * @return string
345
     */
346
    public function getType()
347
    {
348
        return _t(__CLASS__ . '.BlockType', 'Block');
349
    }
350
351
    /**
352
     * Proxy through to configuration setting 'inline_editable'
353
     *
354
     * @return bool
355
     */
356
    public function inlineEditable()
357
    {
358
        return static::config()->get('inline_editable');
359
    }
360
361
    /**
362
     * @param ElementController $controller
363
     *
364
     * @return $this
365
     */
366
    public function setController($controller)
367
    {
368
        $this->controller = $controller;
369
370
        return $this;
371
    }
372
373
    /**
374
     * @throws Exception If the specified controller class doesn't exist
375
     *
376
     * @return ElementController
377
     */
378
    public function getController()
379
    {
380
        if ($this->controller) {
381
            return $this->controller;
382
        }
383
384
        $controllerClass = self::config()->controller_class;
385
386
        if (!class_exists($controllerClass)) {
387
            throw new Exception(
388
                'Could not find controller class ' . $controllerClass . ' as defined in ' . static::class
389
            );
390
        }
391
392
        $this->controller = Injector::inst()->create($controllerClass, $this);
393
        $this->controller->doInit();
394
395
        return $this->controller;
396
    }
397
398
    /**
399
     * @param string $name
400
     * @return $this
401
     */
402
    public function setAreaRelationNameCache($name)
403
    {
404
        $this->cacheData['area_relation_name'] = $name;
405
406
        return $this;
407
    }
408
409
    /**
410
     * @return Controller
411
     */
412
    public function Top()
413
    {
414
        return (Controller::has_curr()) ? Controller::curr() : null;
415
    }
416
417
    /**
418
     * Default way to render element in templates. Note that all blocks should
419
     * be rendered through their {@link ElementController} class as this
420
     * contains the holder styles.
421
     *
422
     * @return string|null HTML
423
     */
424
    public function forTemplate($holder = true)
425
    {
426
        $templates = $this->getRenderTemplates();
427
428
        if ($templates) {
429
            return $this->renderWith($templates);
430
        }
431
432
        return null;
433
    }
434
435
    /**
436
     * @param string $suffix
437
     *
438
     * @return array
439
     */
440
    public function getRenderTemplates($suffix = '')
441
    {
442
        $classes = ClassInfo::ancestry($this->ClassName);
443
        $classes[static::class] = static::class;
444
        $classes = array_reverse($classes);
445
        $templates = [];
446
447
        foreach ($classes as $key => $class) {
448
            if ($class == BaseElement::class) {
449
                continue;
450
            }
451
452
            if ($class == DataObject::class) {
453
                break;
454
            }
455
456
            if ($style = $this->Style) {
457
                $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName() . '_' . $style;
458
                $templates[$class][] = $class . $suffix . '_' . $style;
459
            }
460
            $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName();
461
            $templates[$class][] = $class . $suffix;
462
        }
463
464
        $this->extend('updateRenderTemplates', $templates, $suffix);
465
466
        $templateFlat = [];
467
        foreach ($templates as $class => $variations) {
468
            $templateFlat = array_merge($templateFlat, $variations);
469
        }
470
471
        return $templateFlat;
472
    }
473
474
    /**
475
     * Given form data (wit
476
     *
477
     * @param $data
478
     */
479
    public function updateFromFormData($data)
480
    {
481
        $cmsFields = $this->getCMSFields();
482
483
        foreach ($data as $field => $datum) {
484
            $field = $cmsFields->dataFieldByName($field);
485
486
            if (!$field) {
487
                continue;
488
            }
489
490
            $field->setSubmittedValue($datum);
491
            $field->saveInto($this);
492
        }
493
    }
494
495
    /**
496
     * Strip all namespaces from class namespace.
497
     *
498
     * @param string $classname e.g. "\Fully\Namespaced\Class"
499
     *
500
     * @return string following the param example, "Class"
501
     */
502
    protected function stripNamespacing($classname)
503
    {
504
        $classParts = explode('\\', $classname);
505
        return array_pop($classParts);
506
    }
507
508
    /**
509
     * @return string
510
     */
511
    public function getSimpleClassName()
512
    {
513
        return strtolower($this->sanitiseClassName($this->ClassName, '__'));
514
    }
515
516
    /**
517
     * @return null|SiteTree
518
     * @throws \Psr\Container\NotFoundExceptionInterface
519
     * @throws \SilverStripe\ORM\ValidationException
520
     */
521
    public function getPage()
522
    {
523
        // Allow for repeated calls to be cached
524
        if (isset($this->cacheData['page'])) {
525
            return $this->cacheData['page'];
526
        }
527
528
        $area = $this->Parent();
529
530
        if ($area instanceof ElementalArea && $area->exists()) {
531
            $this->cacheData['page'] = $area->getOwnerPage();
532
            return $this->cacheData['page'];
533
        }
534
535
        return null;
536
    }
537
538
    /**
539
     * Get a unique anchor name
540
     *
541
     * @return string
542
     */
543
    public function getAnchor()
544
    {
545
        if ($this->anchor !== null) {
546
            return $this->anchor;
547
        }
548
549
        $anchorTitle = '';
550
551
        if (!$this->config()->disable_pretty_anchor_name) {
552
            if ($this->hasMethod('getAnchorTitle')) {
553
                $anchorTitle = $this->getAnchorTitle();
554
            } elseif ($this->config()->enable_title_in_template) {
555
                $anchorTitle = $this->getField('Title');
556
            }
557
        }
558
559
        if (!$anchorTitle) {
560
            $anchorTitle = 'e'.$this->ID;
561
        }
562
563
        $filter = URLSegmentFilter::create();
564
        $titleAsURL = $filter->filter($anchorTitle);
565
566
        // Ensure that this anchor name isn't already in use
567
        // ie. If two elemental blocks have the same title, it'll append '-2', '-3'
568
        $result = $titleAsURL;
569
        $count = 1;
570
        while (isset(self::$used_anchors[$result]) && self::$used_anchors[$result] !== $this->ID) {
571
            ++$count;
572
            $result = $titleAsURL . '-' . $count;
573
        }
574
        self::$used_anchors[$result] = $this->ID;
575
        return $this->anchor = $result;
576
    }
577
578
    /**
579
     * @param string|null $action
580
     * @return string|null
581
     * @throws \Psr\Container\NotFoundExceptionInterface
582
     * @throws \SilverStripe\ORM\ValidationException
583
     */
584
    public function AbsoluteLink($action = null)
585
    {
586
        if ($page = $this->getPage()) {
587
            $link = $page->AbsoluteLink($action) . '#' . $this->getAnchor();
588
589
            return $link;
590
        }
591
592
        return null;
593
    }
594
595
    /**
596
     * @param string|null $action
597
     * @return string
598
     * @throws \Psr\Container\NotFoundExceptionInterface
599
     * @throws \SilverStripe\ORM\ValidationException
600
     */
601
    public function Link($action = null)
602
    {
603
        if ($page = $this->getPage()) {
604
            $link = $page->Link($action) . '#' . $this->getAnchor();
605
606
            $this->extend('updateLink', $link);
607
608
            return $link;
609
        }
610
611
        return null;
612
    }
613
614
    /**
615
     * @param string|null $action
616
     * @return string
617
     * @throws \Psr\Container\NotFoundExceptionInterface
618
     * @throws \SilverStripe\ORM\ValidationException
619
     */
620
    public function PreviewLink($action = null)
621
    {
622
        $action = $action . '?ElementalPreview=' . mt_rand();
623
        $link = $this->Link($action);
624
        $this->extend('updatePreviewLink', $link);
625
626
        return $link;
627
    }
628
629
    /**
630
     * @return boolean
631
     */
632
    public function isCMSPreview()
633
    {
634
        if (Controller::has_curr()) {
635
            $controller = Controller::curr();
636
637
            if ($controller->getRequest()->requestVar('CMSPreview')) {
638
                return true;
639
            }
640
        }
641
642
        return false;
643
    }
644
645
    /**
646
     * @return null|string
647
     * @throws \Psr\Container\NotFoundExceptionInterface
648
     * @throws \SilverStripe\ORM\ValidationException
649
     */
650
    public function CMSEditLink()
651
    {
652
        // Allow for repeated calls to be returned from cache
653
        if (isset($this->cacheData['cms_edit_link'])) {
654
            return $this->cacheData['cms_edit_link'];
655
        }
656
657
        $relationName = $this->getAreaRelationName();
658
        $page = $this->getPage();
659
660
        if (!$page) {
661
            return null;
662
        }
663
664
        $editLinkPrefix = '';
665
        if (!$page instanceof SiteTree && method_exists($page, 'CMSEditLink')) {
666
            $link = Controller::join_links($page->CMSEditLink(), 'ItemEditForm');
667
        } else {
668
            $link = Controller::join_links(
669
                singleton(CMSPageEditController::class)->Link('EditForm'),
670
                $page->ID
671
            );
672
        }
673
674
        // In-line editable blocks should just take you to the page. Editable ones should add the suffix for detail form
675
        if (!$this->inlineEditable()) {
676
            $link = Controller::join_links(
677
                $link,
678
                'field/' . $relationName . '/item/',
679
                $this->ID,
680
                'edit'
681
            );
682
        }
683
684
        $this->extend('updateCMSEditLink', $link);
685
686
        $this->cacheData['cms_edit_link'] = $link;
687
        return $link;
688
    }
689
690
    /**
691
     * Retrieve a elemental area relation for creating cms links
692
     *
693
     * @return int|string The name of a valid elemental area relation
694
     * @throws \Psr\Container\NotFoundExceptionInterface
695
     * @throws \SilverStripe\ORM\ValidationException
696
     */
697
    public function getAreaRelationName()
698
    {
699
        // Allow repeated calls to return from internal cache
700
        if (isset($this->cacheData['area_relation_name'])) {
701
            return $this->cacheData['area_relation_name'];
702
        }
703
704
        $page = $this->getPage();
705
706
        $result = 'ElementalArea';
707
708
        if ($page) {
709
            $has_one = $page->config()->get('has_one');
710
            $area = $this->Parent();
711
712
            foreach ($has_one as $relationName => $relationClass) {
713
                if ($page instanceof BaseElement && $relationName === 'Parent') {
714
                    continue;
715
                }
716
                if ($relationClass === $area->ClassName && $page->{$relationName}()->ID === $area->ID) {
717
                    $result = $relationName;
718
                    break;
719
                }
720
            }
721
        }
722
723
        $this->setAreaRelationNameCache($result);
724
725
        return $result;
726
    }
727
728
    /**
729
     * Sanitise a model class' name for inclusion in a link.
730
     *
731
     * @return string
732
     */
733
    public function sanitiseClassName($class, $delimiter = '-')
734
    {
735
        return str_replace('\\', $delimiter, $class);
736
    }
737
738
    public function unsanitiseClassName($class, $delimiter = '-')
739
    {
740
        return str_replace($delimiter, '\\', $class);
741
    }
742
743
    /**
744
     * @return null|string
745
     * @throws \Psr\Container\NotFoundExceptionInterface
746
     * @throws \SilverStripe\ORM\ValidationException
747
     */
748
    public function getEditLink()
749
    {
750
        return $this->CMSEditLink();
751
    }
752
753
    /**
754
     * @return DBField|null
755
     * @throws \Psr\Container\NotFoundExceptionInterface
756
     * @throws \SilverStripe\ORM\ValidationException
757
     */
758
    public function PageCMSEditLink()
759
    {
760
        if ($page = $this->getPage()) {
761
            return DBField::create_field('HTMLText', sprintf(
762
                '<a href="%s">%s</a>',
763
                $page->CMSEditLink(),
764
                $page->Title
765
            ));
766
        }
767
768
        return null;
769
    }
770
771
    /**
772
     * @return string
773
     */
774
    public function getMimeType()
775
    {
776
        return 'text/html';
777
    }
778
779
    /**
780
     * This can be overridden on child elements to create a summary for display
781
     * in GridFields.
782
     *
783
     * @return string
784
     */
785
    public function getSummary()
786
    {
787
        return '';
788
    }
789
790
    /**
791
     * The block config defines a set of data (usually set through config on the element) that will be made available in
792
     * client side config. Individual element types may choose to add config variable for use in React code
793
     *
794
     * @return array
795
     */
796
    public static function getBlockConfig()
797
    {
798
        return [];
799
    }
800
801
    /**
802
     * The block actions is an associative array available for providing data to the client side to be used to describe
803
     * actions that may be performed. This is available as a plain "ObjectType" in the GraphQL schema.
804
     *
805
     * By default the only action is "edit" which is simply the URL where the block may be edited.
806
     *
807
     * To modify the actions, either use the extension point or overload the `provideBlockSchema` method.
808
     *
809
     * @internal This API may change in future. Treat this as a `final` method.
810
     * @return array
811
     */
812
    public function getBlockSchema()
813
    {
814
        $blockSchema = $this->provideBlockSchema();
815
816
        $this->extend('updateBlockSchema', $blockSchema);
817
818
        return $blockSchema;
819
    }
820
821
    /**
822
     * Provide block schema data, which will be serialised and sent via GraphQL to the editor client.
823
     *
824
     * Overload this method in child element classes to augment, or use the extension point on `getBlockSchema`
825
     * to update it from an `Extension`.
826
     *
827
     * @return array
828
     */
829
    protected function provideBlockSchema()
830
    {
831
        return [
832
            // Currently GraphQL doesn't expose the correct type name and just returns "base element"s. This is a
833
            // workaround until we can scaffold a query client side that specifies by type name
834
            'typeName' => StaticSchema::inst()->typeNameForDataObject(static::class),
835
            'actions' => [
836
                'edit' => $this->getEditLink(),
837
            ],
838
        ];
839
    }
840
841
    /**
842
     * Generate markup for element type icons suitable for use in GridFields.
843
     *
844
     * @return null|DBHTMLText
845
     */
846
    public function getIcon()
847
    {
848
        $data = ArrayData::create([]);
849
850
        $iconClass = $this->config()->get('icon');
851
        if ($iconClass) {
852
            $data->IconClass = $iconClass;
853
854
            // Add versioned states (rendered as a circle over the icon)
855
            if ($this->hasExtension(Versioned::class)) {
856
                $data->IsVersioned = true;
857
                if ($this->isOnDraftOnly()) {
0 ignored issues
show
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

857
                if ($this->/** @scrutinizer ignore-call */ isOnDraftOnly()) {
Loading history...
858
                    $data->VersionState = 'draft';
859
                    $data->VersionStateTitle = _t(
860
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.ADDEDTODRAFTHELP',
861
                        'Item has not been published yet'
862
                    );
863
                } elseif ($this->isModifiedOnDraft()) {
0 ignored issues
show
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

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