Passed
Push — master ( b37b7e...cbebc2 )
by Guy
04:54 queued 11s
created

src/Models/BaseElement.php (6 issues)

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\View\ArrayData;
29
use SilverStripe\View\Parsers\URLSegmentFilter;
30
31
/**
32
 * Class BaseElement
33
 * @package DNADesign\Elemental\Models
34
 *
35
 * @property string $Title
36
 * @property bool $ShowTitle
37
 * @property int $Sort
38
 * @property string $ExtraClass
39
 * @property string $Style
40
 *
41
 * @method ElementalArea Parent()
42
 */
43
class BaseElement extends DataObject
44
{
45
    /**
46
     * Override this on your custom elements to specify a CSS icon class
47
     *
48
     * @var string
49
     */
50
    private static $icon = 'font-icon-block-layout';
51
52
    /**
53
     * Describe the purpose of this element
54
     *
55
     * @config
56
     * @var string
57
     */
58
    private static $description = 'Base element class';
59
60
    private static $db = [
61
        'Title' => 'Varchar(255)',
62
        'ShowTitle' => 'Boolean',
63
        'Sort' => 'Int',
64
        'ExtraClass' => 'Varchar(255)',
65
        'Style' => 'Varchar(255)'
66
    ];
67
68
    private static $has_one = [
69
        'Parent' => ElementalArea::class
70
    ];
71
72
    private static $extensions = [
73
        Versioned::class
74
    ];
75
76
    private static $casting = [
77
        'BlockSchema' => DBObjectType::class,
78
        'IsLiveVersion' => DBBoolean::class,
79
        'IsPublished' => DBBoolean::class,
80
    ];
81
82
    private static $versioned_gridfield_extensions = true;
83
84
    private static $table_name = 'Element';
85
86
    /**
87
     * @var string
88
     */
89
    private static $controller_class = ElementController::class;
90
91
    /**
92
     * @var string
93
     */
94
    private static $controller_template = 'ElementHolder';
95
96
    /**
97
     * @var ElementController
98
     */
99
    protected $controller;
100
101
    /**
102
     * Cache various data to improve CMS load time
103
     *
104
     * @internal
105
     * @var array
106
     */
107
    protected $cacheData;
108
109
    private static $default_sort = 'Sort';
110
111
    private static $singular_name = 'block';
112
113
    private static $plural_name = 'blocks';
114
115
    private static $summary_fields = [
116
        'EditorPreview' => 'Summary'
117
    ];
118
119
    /**
120
     * @config
121
     * @var array
122
     */
123
    private static $styles = [];
124
125
    private static $searchable_fields = [
126
        'ID' => [
127
            'field' => NumericField::class,
128
        ],
129
        'Title',
130
        'LastEdited'
131
    ];
132
133
    /**
134
     * Enable for backwards compatibility
135
     *
136
     * @var boolean
137
     */
138
    private static $disable_pretty_anchor_name = false;
139
140
    /**
141
     * Set to false to prevent an in-line edit form from showing in an elemental area. Instead the element will be
142
     * clickable and a GridFieldDetailForm will be used.
143
     *
144
     * @config
145
     * @var bool
146
     */
147
    private static $inline_editable = true;
148
149
    /**
150
     * Store used anchor names, this is to avoid title clashes
151
     * when calling 'getAnchor'
152
     *
153
     * @var array
154
     */
155
    protected static $used_anchors = [];
156
157
    /**
158
     * For caching 'getAnchor'
159
     *
160
     * @var string
161
     */
162
    protected $anchor = null;
163
164
    /**
165
     * Basic permissions, defaults to page perms where possible.
166
     *
167
     * @param Member $member
168
     * @return boolean
169
     */
170
    public function canView($member = null)
171
    {
172
        $extended = $this->extendedCan(__FUNCTION__, $member);
173
        if ($extended !== null) {
174
            return $extended;
175
        }
176
177
        if ($this->hasMethod('getPage')) {
178
            if ($page = $this->getPage()) {
179
                return $page->canView($member);
180
            }
181
        }
182
183
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
184
    }
185
186
    /**
187
     * Basic permissions, defaults to page perms where possible.
188
     *
189
     * @param Member $member
190
     *
191
     * @return boolean
192
     */
193
    public function canEdit($member = null)
194
    {
195
        $extended = $this->extendedCan(__FUNCTION__, $member);
196
        if ($extended !== null) {
197
            return $extended;
198
        }
199
200
        if ($this->hasMethod('getPage')) {
201
            if ($page = $this->getPage()) {
202
                return $page->canEdit($member);
203
            }
204
        }
205
206
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
207
    }
208
209
    /**
210
     * Basic permissions, defaults to page perms where possible.
211
     *
212
     * Uses archive not delete so that current stage is respected i.e if a
213
     * element is not published, then it can be deleted by someone who doesn't
214
     * have publishing permissions.
215
     *
216
     * @param Member $member
217
     *
218
     * @return boolean
219
     */
220
    public function canDelete($member = null)
221
    {
222
        $extended = $this->extendedCan(__FUNCTION__, $member);
223
        if ($extended !== null) {
224
            return $extended;
225
        }
226
227
        if ($this->hasMethod('getPage')) {
228
            if ($page = $this->getPage()) {
229
                return $page->canArchive($member);
230
            }
231
        }
232
233
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
234
    }
235
236
    /**
237
     * Basic permissions, defaults to page perms where possible.
238
     *
239
     * @param Member $member
240
     * @param array $context
241
     *
242
     * @return boolean
243
     */
244
    public function canCreate($member = null, $context = array())
245
    {
246
        $extended = $this->extendedCan(__FUNCTION__, $member);
247
        if ($extended !== null) {
248
            return $extended;
249
        }
250
251
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
252
    }
253
254
    /**
255
     * Increment the sort order if one hasn't been already defined. This
256
     * ensures that new elements are created at the end of the list by default.
257
     *
258
     * {@inheritDoc}
259
     */
260
    public function onBeforeWrite()
261
    {
262
        parent::onBeforeWrite();
263
264
        if (!$this->Sort) {
265
            if ($this->hasExtension(Versioned::class)) {
266
                $records = Versioned::get_by_stage(BaseElement::class, Versioned::DRAFT);
267
            } else {
268
                $records = BaseElement::get();
269
            }
270
271
            $records = $records->filter('ParentID', $this->ParentID);
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __get, consider adding a @property annotation.
Loading history...
272
273
            $this->Sort = $records->max('Sort') + 1;
274
        }
275
    }
276
277
    public function getCMSFields()
278
    {
279
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
280
            // Remove relationship fields
281
            $fields->removeByName('ParentID');
282
            $fields->removeByName('Sort');
283
284
            // Remove link and file tracking tabs
285
            $fields->removeByName(['LinkTracking', 'FileTracking']);
286
287
            $fields->addFieldToTab(
288
                'Root.Settings',
289
                TextField::create('ExtraClass', _t(__CLASS__ . '.ExtraCssClassesLabel', 'Custom CSS classes'))
290
                    ->setAttribute(
291
                        'placeholder',
292
                        _t(__CLASS__ . '.ExtraCssClassesPlaceholder', 'my_class another_class')
293
                    )
294
            );
295
296
            // Rename the "Settings" tab
297
            $fields->fieldByName('Root.Settings')
298
                ->setTitle(_t(__CLASS__ . '.SettingsTabLabel', 'Settings'));
299
300
            // Add a combined field for "Title" and "Displayed" checkbox in a Bootstrap input group
301
            $fields->removeByName('ShowTitle');
302
            $fields->replaceField(
303
                'Title',
304
                TextCheckboxGroupField::create()
305
                    ->setName('Title')
306
            );
307
308
            // Rename the "Main" tab
309
            $fields->fieldByName('Root.Main')
310
                ->setTitle(_t(__CLASS__ . '.MainTabLabel', 'Content'));
311
312
            $fields->addFieldsToTab('Root.Main', [
313
                HiddenField::create('AbsoluteLink', false, Director::absoluteURL($this->PreviewLink())),
314
                HiddenField::create('LiveLink', false, Director::absoluteURL($this->Link())),
315
                HiddenField::create('StageLink', false, Director::absoluteURL($this->PreviewLink())),
316
            ]);
317
318
            $styles = $this->config()->get('styles');
319
320
            if ($styles && count($styles) > 0) {
321
                $styleDropdown = DropdownField::create('Style', _t(__CLASS__.'.STYLE', 'Style variation'), $styles);
322
323
                $fields->insertBefore($styleDropdown, 'ExtraClass');
324
325
                $styleDropdown->setEmptyString(_t(__CLASS__.'.CUSTOM_STYLES', 'Select a style..'));
326
            } else {
327
                $fields->removeByName('Style');
328
            }
329
330
            // Hide the navigation section of the tabs in the React component {@see silverstripe/admin Tabs}
331
            $rootTabset = $fields->fieldByName('Root');
332
            $rootTabset->setSchemaState(['hideNav' => true]);
333
        });
334
335
        return parent::getCMSFields();
336
    }
337
338
    /**
339
     * Get the type of the current block, for use in GridField summaries, block
340
     * type dropdowns etc. Examples are "Content", "File", "Media", etc.
341
     *
342
     * @return string
343
     */
344
    public function getType()
345
    {
346
        return _t(__CLASS__ . '.BlockType', 'Block');
347
    }
348
349
    /**
350
     * Proxy through to configuration setting 'inline_editable'
351
     *
352
     * @return bool
353
     */
354
    public function inlineEditable()
355
    {
356
        return static::config()->get('inline_editable');
357
    }
358
359
    /**
360
     * @param ElementController $controller
361
     *
362
     * @return $this
363
     */
364
    public function setController($controller)
365
    {
366
        $this->controller = $controller;
367
368
        return $this;
369
    }
370
371
    /**
372
     * @throws Exception If the specified controller class doesn't exist
373
     *
374
     * @return ElementController
375
     */
376
    public function getController()
377
    {
378
        if ($this->controller) {
379
            return $this->controller;
380
        }
381
382
        $controllerClass = self::config()->controller_class;
383
384
        if (!class_exists($controllerClass)) {
385
            throw new Exception(
386
                'Could not find controller class ' . $controllerClass . ' as defined in ' . static::class
387
            );
388
        }
389
390
        $this->controller = Injector::inst()->create($controllerClass, $this);
391
        $this->controller->doInit();
392
393
        return $this->controller;
394
    }
395
396
    /**
397
     * @param string $name
398
     * @return $this
399
     */
400
    public function setAreaRelationNameCache($name)
401
    {
402
        $this->cacheData['area_relation_name'] = $name;
403
404
        return $this;
405
    }
406
407
    /**
408
     * @return Controller
409
     */
410
    public function Top()
411
    {
412
        return (Controller::has_curr()) ? Controller::curr() : null;
413
    }
414
415
    /**
416
     * Default way to render element in templates. Note that all blocks should
417
     * be rendered through their {@link ElementController} class as this
418
     * contains the holder styles.
419
     *
420
     * @return string|null HTML
421
     */
422
    public function forTemplate($holder = true)
423
    {
424
        $templates = $this->getRenderTemplates();
425
426
        if ($templates) {
427
            return $this->renderWith($templates);
428
        }
429
430
        return null;
431
    }
432
433
    /**
434
     * @param string $suffix
435
     *
436
     * @return array
437
     */
438
    public function getRenderTemplates($suffix = '')
439
    {
440
        $classes = ClassInfo::ancestry($this->ClassName);
441
        $classes[static::class] = static::class;
442
        $classes = array_reverse($classes);
443
        $templates = [];
444
445
        foreach ($classes as $key => $class) {
446
            if ($class == BaseElement::class) {
447
                continue;
448
            }
449
450
            if ($class == DataObject::class) {
451
                break;
452
            }
453
454
            if ($style = $this->Style) {
455
                $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName() . '_' . $style;
456
                $templates[$class][] = $class . $suffix . '_' . $style;
457
            }
458
            $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName();
459
            $templates[$class][] = $class . $suffix;
460
        }
461
462
        $this->extend('updateRenderTemplates', $templates, $suffix);
463
464
        $templateFlat = [];
465
        foreach ($templates as $class => $variations) {
466
            $templateFlat = array_merge($templateFlat, $variations);
467
        }
468
469
        return $templateFlat;
470
    }
471
472
    /**
473
     * Given form data (wit
474
     *
475
     * @param $data
476
     */
477
    public function updateFromFormData($data)
478
    {
479
        $cmsFields = $this->getCMSFields();
480
481
        foreach ($data as $field => $datum) {
482
            $field = $cmsFields->dataFieldByName($field);
483
484
            if (!$field) {
485
                continue;
486
            }
487
488
            $field->setSubmittedValue($datum);
489
            $field->saveInto($this);
490
        }
491
    }
492
493
    /**
494
     * Strip all namespaces from class namespace.
495
     *
496
     * @param string $classname e.g. "\Fully\Namespaced\Class"
497
     *
498
     * @return string following the param example, "Class"
499
     */
500
    protected function stripNamespacing($classname)
501
    {
502
        $classParts = explode('\\', $classname);
503
        return array_pop($classParts);
504
    }
505
506
    /**
507
     * @return string
508
     */
509
    public function getSimpleClassName()
510
    {
511
        return strtolower($this->sanitiseClassName($this->ClassName, '__'));
512
    }
513
514
    /**
515
     * @return null|SiteTree
516
     * @throws \Psr\Container\NotFoundExceptionInterface
517
     * @throws \SilverStripe\ORM\ValidationException
518
     */
519
    public function getPage()
520
    {
521
        // Allow for repeated calls to be cached
522
        if (isset($this->cacheData['page'])) {
523
            return $this->cacheData['page'];
524
        }
525
526
527
        $class = DataObject::getSchema()->hasOneComponent($this, 'Parent');
528
        $area = ($this->ParentID) ? DataObject::get_by_id($class, $this->ParentID) : null;
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __get, consider adding a @property annotation.
Loading history...
529
        if ($area instanceof ElementalArea && $area->exists()) {
530
            $this->cacheData['page'] = $area->getOwnerPage();
531
            return $this->cacheData['page'];
532
        }
533
534
        return null;
535
    }
536
537
    /**
538
     * Get a unique anchor name
539
     *
540
     * @return string
541
     */
542
    public function getAnchor()
543
    {
544
        if ($this->anchor !== null) {
545
            return $this->anchor;
546
        }
547
548
        $anchorTitle = '';
549
550
        if (!$this->config()->disable_pretty_anchor_name) {
551
            if ($this->hasMethod('getAnchorTitle')) {
552
                $anchorTitle = $this->getAnchorTitle();
0 ignored issues
show
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

552
                /** @scrutinizer ignore-call */ 
553
                $anchorTitle = $this->getAnchorTitle();
Loading history...
553
            } elseif ($this->config()->enable_title_in_template) {
554
                $anchorTitle = $this->getField('Title');
555
            }
556
        }
557
558
        if (!$anchorTitle) {
559
            $anchorTitle = 'e'.$this->ID;
560
        }
561
562
        $filter = URLSegmentFilter::create();
563
        $titleAsURL = $filter->filter($anchorTitle);
564
565
        // Ensure that this anchor name isn't already in use
566
        // ie. If two elemental blocks have the same title, it'll append '-2', '-3'
567
        $result = $titleAsURL;
568
        $count = 1;
569
        while (isset(self::$used_anchors[$result]) && self::$used_anchors[$result] !== $this->ID) {
570
            ++$count;
571
            $result = $titleAsURL . '-' . $count;
572
        }
573
        self::$used_anchors[$result] = $this->ID;
574
        return $this->anchor = $result;
575
    }
576
577
    /**
578
     * @param string|null $action
579
     * @return string|null
580
     * @throws \Psr\Container\NotFoundExceptionInterface
581
     * @throws \SilverStripe\ORM\ValidationException
582
     */
583
    public function AbsoluteLink($action = null)
584
    {
585
        if ($page = $this->getPage()) {
586
            $link = $page->AbsoluteLink($action) . '#' . $this->getAnchor();
587
588
            return $link;
589
        }
590
591
        return null;
592
    }
593
594
    /**
595
     * @param string|null $action
596
     * @return string
597
     * @throws \Psr\Container\NotFoundExceptionInterface
598
     * @throws \SilverStripe\ORM\ValidationException
599
     */
600
    public function Link($action = null)
601
    {
602
        if ($page = $this->getPage()) {
603
            $link = $page->Link($action) . '#' . $this->getAnchor();
604
605
            $this->extend('updateLink', $link);
606
607
            return $link;
608
        }
609
610
        return null;
611
    }
612
613
    /**
614
     * @param string|null $action
615
     * @return string
616
     * @throws \Psr\Container\NotFoundExceptionInterface
617
     * @throws \SilverStripe\ORM\ValidationException
618
     */
619
    public function PreviewLink($action = null)
620
    {
621
        $action = $action . '?ElementalPreview=' . mt_rand();
622
        $link = $this->Link($action);
623
        $this->extend('updatePreviewLink', $link);
624
625
        return $link;
626
    }
627
628
    /**
629
     * @return boolean
630
     */
631
    public function isCMSPreview()
632
    {
633
        if (Controller::has_curr()) {
634
            $controller = Controller::curr();
635
636
            if ($controller->getRequest()->requestVar('CMSPreview')) {
637
                return true;
638
            }
639
        }
640
641
        return false;
642
    }
643
644
    /**
645
     * @return null|string
646
     * @throws \Psr\Container\NotFoundExceptionInterface
647
     * @throws \SilverStripe\ORM\ValidationException
648
     */
649
    public function CMSEditLink()
650
    {
651
        // Allow for repeated calls to be returned from cache
652
        if (isset($this->cacheData['cms_edit_link'])) {
653
            return $this->cacheData['cms_edit_link'];
654
        }
655
656
        $relationName = $this->getAreaRelationName();
657
        $page = $this->getPage();
658
659
        if (!$page) {
660
            return null;
661
        }
662
663
        if (!$page instanceof SiteTree && method_exists($page, 'CMSEditLink')) {
664
            $link = Controller::join_links($page->CMSEditLink(), 'ItemEditForm');
665
        } else {
666
            $link = $page->CMSEditLink();
667
        }
668
669
        // In-line editable blocks should just take you to the page. Editable ones should add the suffix for detail form
670
        if (!$this->inlineEditable()) {
671
            $link = Controller::join_links(
672
                singleton(CMSPageEditController::class)->Link('EditForm'),
673
                $page->ID,
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 Director::absoluteURL($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
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
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
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