Passed
Pull Request — 4 (#948)
by
unknown
02:51
created

BaseElement::canView()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 7
c 0
b 0
f 0
nc 6
nop 1
dl 0
loc 14
rs 9.6111
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\GraphQL\Schema\Exception\SchemaBuilderException;
22
use SilverStripe\ORM\DataObject;
23
use SilverStripe\ORM\FieldType\DBBoolean;
24
use SilverStripe\ORM\FieldType\DBField;
25
use SilverStripe\ORM\FieldType\DBHTMLText;
26
use SilverStripe\ORM\ValidationException;
27
use SilverStripe\Security\Member;
28
use SilverStripe\Security\Permission;
29
use SilverStripe\Versioned\Versioned;
30
use SilverStripe\VersionedAdmin\Forms\HistoryViewerField;
31
use SilverStripe\View\ArrayData;
32
use SilverStripe\View\Parsers\URLSegmentFilter;
33
use SilverStripe\View\Requirements;
34
35
/**
36
 * Class BaseElement
37
 * @package DNADesign\Elemental\Models
38
 *
39
 * @property string $Title
40
 * @property bool $ShowTitle
41
 * @property int $Sort
42
 * @property string $ExtraClass
43
 * @property string $Style
44
 * @property int $ParentID
45
 *
46
 * @method ElementalArea Parent()
47
 *
48
 * @mixin Versioned
49
 */
50
class BaseElement extends DataObject
51
{
52
    /**
53
     * Override this on your custom elements to specify a CSS icon class
54
     *
55
     * @var string
56
     */
57
    private static $icon = 'font-icon-block-layout';
58
59
    /**
60
     * Describe the purpose of this element
61
     *
62
     * @config
63
     * @var string
64
     */
65
    private static $description = 'Base element class';
66
67
    private static $db = [
68
        'Title' => 'Varchar(255)',
69
        'ShowTitle' => 'Boolean',
70
        'Sort' => 'Int',
71
        'ExtraClass' => 'Varchar(255)',
72
        'Style' => 'Varchar(255)'
73
    ];
74
75
    private static $has_one = [
76
        'Parent' => ElementalArea::class
77
    ];
78
79
    private static $extensions = [
80
        Versioned::class
81
    ];
82
83
    private static $casting = [
84
        'BlockSchema' => DBObjectType::class,
85
        'IsLiveVersion' => DBBoolean::class,
86
        'IsPublished' => DBBoolean::class,
87
        'canCreate' => DBBoolean::class,
88
        'canPublish' => DBBoolean::class,
89
        'canUnpublish' => DBBoolean::class,
90
        'canDelete' => DBBoolean::class,
91
    ];
92
93
    private static $indexes = [
94
        'Sort' => true,
95
    ];
96
97
    private static $versioned_gridfield_extensions = true;
98
99
    private static $table_name = 'Element';
100
101
    /**
102
     * @var string
103
     */
104
    private static $controller_class = ElementController::class;
105
106
    /**
107
     * @var string
108
     */
109
    private static $controller_template = 'ElementHolder';
110
111
    /**
112
     * @var ElementController
113
     */
114
    protected $controller;
115
116
    /**
117
     * Cache various data to improve CMS load time
118
     *
119
     * @internal
120
     * @var array
121
     */
122
    protected $cacheData;
123
124
    private static $default_sort = 'Sort';
125
126
    private static $singular_name = 'block';
127
128
    private static $plural_name = 'blocks';
129
130
    private static $summary_fields = [
131
        'EditorPreview' => 'Summary'
132
    ];
133
134
    /**
135
     * @config
136
     * @var array
137
     */
138
    private static $styles = [];
139
140
    private static $searchable_fields = [
141
        'ID' => [
142
            'field' => NumericField::class,
143
        ],
144
        'Title',
145
        'LastEdited'
146
    ];
147
148
    /**
149
     * Enable for backwards compatibility
150
     *
151
     * @var boolean
152
     */
153
    private static $disable_pretty_anchor_name = false;
154
155
    /**
156
     * Set to false to prevent an in-line edit form from showing in an elemental area. Instead the element will be
157
     * clickable and a GridFieldDetailForm will be used.
158
     *
159
     * @config
160
     * @var bool
161
     */
162
    private static $inline_editable = true;
163
164
    /**
165
     * Display a show title button
166
     *
167
     * @config
168
     * @var boolean
169
     */
170
    private static $displays_title_in_template = true;
171
172
    /**
173
     * Store used anchor names, this is to avoid title clashes
174
     * when calling 'getAnchor'
175
     *
176
     * @var array
177
     */
178
    protected static $used_anchors = [];
179
180
    /**
181
     * For caching 'getAnchor'
182
     *
183
     * @var string
184
     */
185
    protected $anchor = null;
186
187
    /**
188
     * Basic permissions, defaults to page perms where possible.
189
     *
190
     * @param Member $member
191
     * @return boolean
192
     */
193
    public function canView($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->canView($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
     * @param Member $member
213
     *
214
     * @return boolean
215
     */
216
    public function canEdit($member = null)
217
    {
218
        $extended = $this->extendedCan(__FUNCTION__, $member);
219
        if ($extended !== null) {
220
            return $extended;
221
        }
222
223
        if ($this->hasMethod('getPage')) {
224
            if ($page = $this->getPage()) {
225
                return $page->canEdit($member);
226
            }
227
        }
228
229
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
230
    }
231
232
    /**
233
     * Basic permissions, defaults to page perms where possible.
234
     *
235
     * Uses archive not delete so that current stage is respected i.e if a
236
     * element is not published, then it can be deleted by someone who doesn't
237
     * have publishing permissions.
238
     *
239
     * @param Member $member
240
     *
241
     * @return boolean
242
     */
243
    public function canDelete($member = null)
244
    {
245
        $extended = $this->extendedCan(__FUNCTION__, $member);
246
        if ($extended !== null) {
247
            return $extended;
248
        }
249
250
        if ($this->hasMethod('getPage')) {
251
            if ($page = $this->getPage()) {
252
                return $page->canArchive($member);
253
            }
254
        }
255
256
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
257
    }
258
259
    /**
260
     * Basic permissions, defaults to page perms where possible.
261
     *
262
     * @param Member $member
263
     * @param array $context
264
     *
265
     * @return boolean
266
     */
267
    public function canCreate($member = null, $context = array())
268
    {
269
        $extended = $this->extendedCan(__FUNCTION__, $member);
270
        if ($extended !== null) {
271
            return $extended;
272
        }
273
274
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
275
    }
276
277
    /**
278
     * Increment the sort order if one hasn't been already defined. This
279
     * ensures that new elements are created at the end of the list by default.
280
     *
281
     * {@inheritDoc}
282
     */
283
    public function onBeforeWrite()
284
    {
285
        parent::onBeforeWrite();
286
287
        // If a Sort has already been set, then we can exit early
288
        if ($this->Sort) {
289
            return;
290
        }
291
292
        // If no ParentID is currently set for the Element, then we don't want to define an initial Sort yet
293
        if (!$this->ParentID) {
294
            return;
295
        }
296
297
        if ($this->hasExtension(Versioned::class)) {
298
            $records = Versioned::get_by_stage(BaseElement::class, Versioned::DRAFT);
299
        } else {
300
            $records = BaseElement::get();
301
        }
302
303
        $records = $records->filter('ParentID', $this->ParentID);
304
305
        $this->Sort = $records->max('Sort') + 1;
306
    }
307
308
    public function getCMSFields()
309
    {
310
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
311
            // Remove relationship fields
312
            $fields->removeByName('ParentID');
313
            $fields->removeByName('Sort');
314
315
            // Remove link and file tracking tabs
316
            $fields->removeByName(['LinkTracking', 'FileTracking']);
317
318
            $fields->addFieldToTab(
319
                'Root.Settings',
320
                TextField::create('ExtraClass', _t(__CLASS__ . '.ExtraCssClassesLabel', 'Custom CSS classes'))
321
                    ->setAttribute(
322
                        'placeholder',
323
                        _t(__CLASS__ . '.ExtraCssClassesPlaceholder', 'my_class another_class')
324
                    )
325
            );
326
327
            // Rename the "Settings" tab
328
            $fields->fieldByName('Root.Settings')
329
                ->setTitle(_t(__CLASS__ . '.SettingsTabLabel', 'Settings'));
330
331
            // Add a combined field for "Title" and "Displayed" checkbox in a Bootstrap input group
332
            $fields->removeByName('ShowTitle');
333
334
            if ($this->config()->get('displays_title_in_template')) {
335
                $fields->replaceField(
336
                    'Title',
337
                    TextCheckboxGroupField::create()
338
                        ->setName('Title')
339
                );
340
            }
341
342
            // Rename the "Main" tab
343
            $fields->fieldByName('Root.Main')
344
                ->setTitle(_t(__CLASS__ . '.MainTabLabel', 'Content'));
345
346
            $fields->addFieldsToTab('Root.Main', [
347
                HiddenField::create('AbsoluteLink', false, Director::absoluteURL($this->PreviewLink())),
348
                HiddenField::create('LiveLink', false, Director::absoluteURL($this->Link())),
349
                HiddenField::create('StageLink', false, Director::absoluteURL($this->PreviewLink())),
350
            ]);
351
352
            $styles = $this->config()->get('styles');
353
354
            if ($styles && count($styles) > 0) {
355
                $styleDropdown = DropdownField::create('Style', _t(__CLASS__.'.STYLE', 'Style variation'), $styles);
356
357
                $fields->insertBefore($styleDropdown, 'ExtraClass');
358
359
                $styleDropdown->setEmptyString(_t(__CLASS__.'.CUSTOM_STYLES', 'Select a style..'));
360
            } else {
361
                $fields->removeByName('Style');
362
            }
363
364
            // Hide the navigation section of the tabs in the React component {@see silverstripe/admin Tabs}
365
            $rootTabset = $fields->fieldByName('Root');
366
            $rootTabset->setSchemaState(['hideNav' => true]);
367
368
            if ($this->isInDB()) {
369
                $fields->addFieldsToTab('Root.History', [
370
                    HistoryViewerField::create('ElementHistory'),
371
                ]);
372
                // Add class to containing tab
373
                $fields->fieldByName('Root.History')
374
                    ->addExtraClass('elemental-block__history-tab tab--history-viewer');
375
376
                // Hack: automatically navigate to the History tab with `#Root_History` is in the URL
377
                // To unhack, fix this: https://github.com/silverstripe/silverstripe-admin/issues/911
378
                Requirements::customScript(<<<JS
379
    document.addEventListener('DOMContentLoaded', () => {
380
        var hash = window.location.hash.substr(1);
381
        if (hash !== 'Root_History') {
382
            return null;
383
        }
384
        jQuery('.cms-tabset-nav-primary li[aria-controls="Root_History"] a').trigger('click')
385
    });
386
JS
387
                );
388
            }
389
        });
390
391
        return parent::getCMSFields();
392
    }
393
394
    /**
395
     * Get the type of the current block, for use in GridField summaries, block
396
     * type dropdowns etc. Examples are "Content", "File", "Media", etc.
397
     *
398
     * @return string
399
     */
400
    public function getType()
401
    {
402
        return _t(__CLASS__ . '.BlockType', 'Block');
403
    }
404
405
    /**
406
     * Proxy through to configuration setting 'inline_editable'
407
     *
408
     * @return bool
409
     */
410
    public function inlineEditable()
411
    {
412
        return static::config()->get('inline_editable');
413
    }
414
415
    /**
416
     * @param ElementController $controller
417
     *
418
     * @return $this
419
     */
420
    public function setController($controller)
421
    {
422
        $this->controller = $controller;
423
424
        return $this;
425
    }
426
427
    /**
428
     * @throws Exception If the specified controller class doesn't exist
429
     *
430
     * @return ElementController
431
     */
432
    public function getController()
433
    {
434
        if ($this->controller) {
435
            return $this->controller;
436
        }
437
438
        $controllerClass = self::config()->controller_class;
439
440
        if (!class_exists($controllerClass)) {
441
            throw new Exception(
442
                'Could not find controller class ' . $controllerClass . ' as defined in ' . static::class
443
            );
444
        }
445
446
        $this->controller = Injector::inst()->create($controllerClass, $this);
447
        $this->controller->doInit();
448
449
        return $this->controller;
450
    }
451
452
    /**
453
     * @param string $name
454
     * @return $this
455
     */
456
    public function setAreaRelationNameCache($name)
457
    {
458
        $this->cacheData['area_relation_name'] = $name;
459
460
        return $this;
461
    }
462
463
    /**
464
     * @return Controller
465
     */
466
    public function Top()
467
    {
468
        return (Controller::has_curr()) ? Controller::curr() : null;
469
    }
470
471
    /**
472
     * Default way to render element in templates. Note that all blocks should
473
     * be rendered through their {@link ElementController} class as this
474
     * contains the holder styles.
475
     *
476
     * @return string|null HTML
477
     */
478
    public function forTemplate($holder = true)
479
    {
480
        $templates = $this->getRenderTemplates();
481
482
        if ($templates) {
483
            return $this->renderWith($templates);
484
        }
485
486
        return null;
487
    }
488
489
    /**
490
     * @param string $suffix
491
     *
492
     * @return array
493
     */
494
    public function getRenderTemplates($suffix = '')
495
    {
496
        $classes = ClassInfo::ancestry($this->ClassName);
497
        $classes[static::class] = static::class;
498
        $classes = array_reverse($classes);
499
        $templates = [];
500
501
        foreach ($classes as $key => $class) {
502
            if ($class == BaseElement::class) {
503
                continue;
504
            }
505
506
            if ($class == DataObject::class) {
507
                break;
508
            }
509
510
            if ($style = $this->Style) {
511
                $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName() . '_' . $style;
512
                $templates[$class][] = $class . $suffix . '_' . $style;
513
            }
514
            $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName();
515
            $templates[$class][] = $class . $suffix;
516
        }
517
518
        $this->extend('updateRenderTemplates', $templates, $suffix);
519
520
        $templateFlat = [];
521
        foreach ($templates as $class => $variations) {
522
            $templateFlat = array_merge($templateFlat, $variations);
523
        }
524
525
        return $templateFlat;
526
    }
527
528
    /**
529
     * Given form data (wit
530
     *
531
     * @param $data
532
     */
533
    public function updateFromFormData($data)
534
    {
535
        $cmsFields = $this->getCMSFields();
536
537
        foreach ($data as $field => $datum) {
538
            $field = $cmsFields->dataFieldByName($field);
539
540
            if (!$field) {
541
                continue;
542
            }
543
544
            $field->setSubmittedValue($datum);
545
            $field->saveInto($this);
546
        }
547
    }
548
549
    /**
550
     * Strip all namespaces from class namespace.
551
     *
552
     * @param string $classname e.g. "\Fully\Namespaced\Class"
553
     *
554
     * @return string following the param example, "Class"
555
     */
556
    protected function stripNamespacing($classname)
557
    {
558
        $classParts = explode('\\', $classname);
559
        return array_pop($classParts);
560
    }
561
562
    /**
563
     * @return string
564
     */
565
    public function getSimpleClassName()
566
    {
567
        return strtolower($this->sanitiseClassName($this->ClassName, '__'));
568
    }
569
570
    /**
571
     * @return null|SiteTree
572
     * @throws \Psr\Container\NotFoundExceptionInterface
573
     * @throws \SilverStripe\ORM\ValidationException
574
     */
575
    public function getPage()
576
    {
577
        // Allow for repeated calls to be cached
578
        if (isset($this->cacheData['page'])) {
579
            return $this->cacheData['page'];
580
        }
581
582
583
        $class = DataObject::getSchema()->hasOneComponent($this, 'Parent');
584
        $area = ($this->ParentID) ? DataObject::get_by_id($class, $this->ParentID) : null;
585
        if ($area instanceof ElementalArea && $area->exists()) {
586
            $this->cacheData['page'] = $area->getOwnerPage();
587
            return $this->cacheData['page'];
588
        }
589
590
        return null;
591
    }
592
593
    /**
594
     * Get a unique anchor name
595
     *
596
     * @return string
597
     */
598
    public function getAnchor()
599
    {
600
        if ($this->anchor !== null) {
601
            return $this->anchor;
602
        }
603
604
        $anchorTitle = '';
605
606
        if (!$this->config()->disable_pretty_anchor_name) {
607
            if ($this->hasMethod('getAnchorTitle')) {
608
                $anchorTitle = $this->getAnchorTitle();
609
            } elseif ($this->config()->enable_title_in_template) {
610
                $anchorTitle = $this->getField('Title');
611
            }
612
        }
613
614
        if (!$anchorTitle) {
615
            $anchorTitle = 'e'.$this->ID;
616
        }
617
618
        $filter = URLSegmentFilter::create();
619
        $titleAsURL = $filter->filter($anchorTitle);
620
621
        // Ensure that this anchor name isn't already in use
622
        // ie. If two elemental blocks have the same title, it'll append '-2', '-3'
623
        $result = $titleAsURL;
624
        $count = 1;
625
        while (isset(self::$used_anchors[$result]) && self::$used_anchors[$result] !== $this->ID) {
626
            ++$count;
627
            $result = $titleAsURL . '-' . $count;
628
        }
629
        self::$used_anchors[$result] = $this->ID;
630
        return $this->anchor = $result;
631
    }
632
633
    /**
634
     * @param string|null $action
635
     * @return string|null
636
     * @throws \Psr\Container\NotFoundExceptionInterface
637
     * @throws \SilverStripe\ORM\ValidationException
638
     */
639
    public function AbsoluteLink($action = null)
640
    {
641
        if ($page = $this->getPage()) {
642
            $link = $page->AbsoluteLink($action) . '#' . $this->getAnchor();
643
644
            $this->extend('updateAbsoluteLink', $link);
645
646
            return $link;
647
        }
648
649
        return null;
650
    }
651
652
    /**
653
     * @param string|null $action
654
     * @return string
655
     * @throws \Psr\Container\NotFoundExceptionInterface
656
     * @throws \SilverStripe\ORM\ValidationException
657
     */
658
    public function Link($action = null)
659
    {
660
        if ($page = $this->getPage()) {
661
            $link = $page->Link($action) . '#' . $this->getAnchor();
662
663
            $this->extend('updateLink', $link);
664
665
            return $link;
666
        }
667
668
        return null;
669
    }
670
671
    /**
672
     * @param string|null $action
673
     * @return string
674
     * @throws \Psr\Container\NotFoundExceptionInterface
675
     * @throws \SilverStripe\ORM\ValidationException
676
     */
677
    public function PreviewLink($action = null)
678
    {
679
        $action = $action . '?ElementalPreview=' . mt_rand();
680
        $link = $this->Link($action);
681
        $this->extend('updatePreviewLink', $link);
682
683
        return $link;
684
    }
685
686
    /**
687
     * @return boolean
688
     */
689
    public function isCMSPreview()
690
    {
691
        if (Controller::has_curr()) {
692
            $controller = Controller::curr();
693
694
            if ($controller->getRequest()->requestVar('CMSPreview')) {
695
                return true;
696
            }
697
        }
698
699
        return false;
700
    }
701
702
    /**
703
     * @param bool $directLink Indicates that the GridFieldDetailEdit form link should be given even if the block can be
704
     *                         edited in-line.
705
     * @return null|string
706
     * @throws \SilverStripe\ORM\ValidationException
707
     */
708
    public function CMSEditLink($directLink = false)
709
    {
710
        // Allow for repeated calls to be returned from cache
711
        if (isset($this->cacheData['cms_edit_link'])) {
712
            return $this->cacheData['cms_edit_link'];
713
        }
714
715
        $relationName = $this->getAreaRelationName();
716
        $page = $this->getPage();
717
718
        if (!$page) {
719
            $link = null;
720
            $this->extend('updateCMSEditLink', $link);
721
            return $link;
722
        }
723
724
        if (!$page instanceof SiteTree && method_exists($page, 'CMSEditLink')) {
725
            $link = Controller::join_links($page->CMSEditLink(), 'ItemEditForm');
726
        } else {
727
            $link = $page->CMSEditLink();
728
        }
729
730
        // In-line editable blocks should just take you to the page. Editable ones should add the suffix for detail form
731
        if (!$this->inlineEditable() || $directLink) {
732
            $link = Controller::join_links(
733
                singleton(CMSPageEditController::class)->Link('EditForm'),
734
                $page->ID,
735
                'field/' . $relationName . '/item/',
736
                $this->ID,
737
                'edit'
738
            );
739
        }
740
741
        $this->extend('updateCMSEditLink', $link);
742
743
        $this->cacheData['cms_edit_link'] = $link;
744
        return $link;
745
    }
746
747
    /**
748
     * Retrieve a elemental area relation for creating cms links
749
     *
750
     * @return int|string The name of a valid elemental area relation
751
     * @throws \Psr\Container\NotFoundExceptionInterface
752
     * @throws \SilverStripe\ORM\ValidationException
753
     */
754
    public function getAreaRelationName()
755
    {
756
        // Allow repeated calls to return from internal cache
757
        if (isset($this->cacheData['area_relation_name'])) {
758
            return $this->cacheData['area_relation_name'];
759
        }
760
761
        $page = $this->getPage();
762
763
        $result = 'ElementalArea';
764
765
        if ($page) {
766
            $has_one = $page->config()->get('has_one');
767
            $area = $this->Parent();
768
769
            foreach ($has_one as $relationName => $relationClass) {
770
                if ($page instanceof BaseElement && $relationName === 'Parent') {
771
                    continue;
772
                }
773
                if ($relationClass === $area->ClassName && $page->{$relationName}()->ID === $area->ID) {
774
                    $result = $relationName;
775
                    break;
776
                }
777
            }
778
        }
779
780
        $this->setAreaRelationNameCache($result);
781
782
        return $result;
783
    }
784
785
    /**
786
     * Sanitise a model class' name for inclusion in a link.
787
     *
788
     * @return string
789
     */
790
    public function sanitiseClassName($class, $delimiter = '-')
791
    {
792
        return str_replace('\\', $delimiter, $class);
793
    }
794
795
    public function unsanitiseClassName($class, $delimiter = '-')
796
    {
797
        return str_replace($delimiter, '\\', $class);
798
    }
799
800
    /**
801
     * @return null|string
802
     * @throws \Psr\Container\NotFoundExceptionInterface
803
     * @throws \SilverStripe\ORM\ValidationException
804
     */
805
    public function getEditLink()
806
    {
807
        return Director::absoluteURL($this->CMSEditLink());
808
    }
809
810
    /**
811
     * @return DBField|null
812
     * @throws \Psr\Container\NotFoundExceptionInterface
813
     * @throws \SilverStripe\ORM\ValidationException
814
     */
815
    public function PageCMSEditLink()
816
    {
817
        if ($page = $this->getPage()) {
818
            return DBField::create_field('HTMLText', sprintf(
819
                '<a href="%s">%s</a>',
820
                $page->CMSEditLink(),
821
                $page->Title
822
            ));
823
        }
824
825
        return null;
826
    }
827
828
    /**
829
     * @return string
830
     */
831
    public function getMimeType()
832
    {
833
        return 'text/html';
834
    }
835
836
    /**
837
     * This can be overridden on child elements to create a summary for display in GridFields.
838
     * The react Summary component takes `content` (html) and/or `fileUrl` & `fileTitle` (image) props,
839
     * which have to be added to the graphql output in `provideBlockSchema()`.
840
     *
841
     * @return string
842
     */
843
    public function getSummary()
844
    {
845
        return '';
846
    }
847
848
    /**
849
     * The block config defines a set of data (usually set through config on the element) that will be made available in
850
     * client side config. Individual element types may choose to add config variable for use in React code
851
     *
852
     * @return array
853
     */
854
    public static function getBlockConfig()
855
    {
856
        return [];
857
    }
858
859
    /**
860
     * The block actions is an associative array available for providing data to the client side to be used to describe
861
     * actions that may be performed. This is available as a plain "ObjectType" in the GraphQL schema.
862
     *
863
     * By default the only action is "edit" which is simply the URL where the block may be edited.
864
     *
865
     * To modify the actions, either use the extension point or overload the `provideBlockSchema` method.
866
     *
867
     * @internal This API may change in future. Treat this as a `final` method.
868
     * @return array
869
     */
870
    public function getBlockSchema()
871
    {
872
        $blockSchema = $this->provideBlockSchema();
873
874
        $this->extend('updateBlockSchema', $blockSchema);
875
876
        return $blockSchema;
877
    }
878
879
    /**
880
     * Provide block schema data, which will be serialised and sent via GraphQL to the editor client.
881
     *
882
     * Overload this method in child element classes to augment, or use the extension point on `getBlockSchema`
883
     * to update it from an `Extension`.
884
     *
885
     * @return array
886
     * @throws SchemaBuilderException
887
     * @throws ValidationException
888
     */
889
    protected function provideBlockSchema()
890
    {
891
        return [
892
            'typeName' => static::getGraphQLTypeName(),
893
            'actions' => [
894
                'edit' => $this->getEditLink(),
895
            ],
896
            'content' = $this->getSummary(),
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected '=', expecting ',' or ']' on line 896 at column 22
Loading history...
897
        ];
898
    }
899
900
    /**
901
     * Generate markup for element type icons suitable for use in GridFields.
902
     *
903
     * @return null|DBHTMLText
904
     */
905
    public function getIcon()
906
    {
907
        $data = ArrayData::create([]);
908
909
        $iconClass = $this->config()->get('icon');
910
        if ($iconClass) {
911
            $data->IconClass = $iconClass;
912
913
            // Add versioned states (rendered as a circle over the icon)
914
            if ($this->hasExtension(Versioned::class)) {
915
                $data->IsVersioned = true;
916
                if ($this->isOnDraftOnly()) {
917
                    $data->VersionState = 'draft';
918
                    $data->VersionStateTitle = _t(
919
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.ADDEDTODRAFTHELP',
920
                        'Item has not been published yet'
921
                    );
922
                } elseif ($this->isModifiedOnDraft()) {
923
                    $data->VersionState = 'modified';
924
                    $data->VersionStateTitle = $data->VersionStateTitle = _t(
925
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.MODIFIEDONDRAFTHELP',
926
                        'Item has unpublished changes'
927
                    );
928
                }
929
            }
930
931
            return $data->renderWith(__CLASS__ . '/PreviewIcon');
932
        }
933
934
        return null;
935
    }
936
937
    /**
938
     * Get a description for this content element, if available
939
     *
940
     * @return string
941
     */
942
    public function getDescription()
943
    {
944
        $description = $this->config()->uninherited('description');
945
        if ($description) {
946
            return _t(__CLASS__ . '.Description', $description);
947
        }
948
        return '';
949
    }
950
951
    /**
952
     * Generate markup for element type, with description suitable for use in
953
     * GridFields.
954
     *
955
     * @return DBField
956
     */
957
    public function getTypeNice()
958
    {
959
        $description = $this->getDescription();
960
        $desc = ($description) ? ' <span class="element__note"> &mdash; ' . $description . '</span>' : '';
961
962
        return DBField::create_field(
963
            'HTMLVarchar',
964
            $this->getType() . $desc
965
        );
966
    }
967
968
    /**
969
     * @return \SilverStripe\ORM\FieldType\DBHTMLText
970
     */
971
    public function getEditorPreview()
972
    {
973
        $templates = $this->getRenderTemplates('_EditorPreview');
974
        $templates[] = BaseElement::class . '_EditorPreview';
975
976
        return $this->renderWith($templates);
977
    }
978
979
    /**
980
     * @return Member
981
     */
982
    public function getAuthor()
983
    {
984
        if ($this->AuthorID) {
985
            return Member::get()->byId($this->AuthorID);
986
        }
987
988
        return null;
989
    }
990
991
    /**
992
     * Get a user defined style variant for this element, if available
993
     *
994
     * @return string
995
     */
996
    public function getStyleVariant()
997
    {
998
        $style = $this->Style;
999
        $styles = $this->config()->get('styles');
1000
1001
        if (isset($styles[$style])) {
1002
            $style = strtolower($style);
1003
        } else {
1004
            $style = '';
1005
        }
1006
1007
        $this->extend('updateStyleVariant', $style);
1008
1009
        return $style;
1010
    }
1011
1012
    /**
1013
     * @return mixed|null
1014
     * @throws \Psr\Container\NotFoundExceptionInterface
1015
     * @throws \SilverStripe\ORM\ValidationException
1016
     */
1017
    public function getPageTitle()
1018
    {
1019
        $page = $this->getPage();
1020
1021
        if ($page) {
1022
            return $page->Title;
1023
        }
1024
1025
        return null;
1026
    }
1027
1028
    /**
1029
     * @return boolean
1030
     */
1031
    public function First()
1032
    {
1033
        return ($this->Parent()->Elements()->first()->ID === $this->ID);
1034
    }
1035
1036
    /**
1037
     * @return boolean
1038
     */
1039
    public function Last()
1040
    {
1041
        return ($this->Parent()->Elements()->last()->ID === $this->ID);
1042
    }
1043
1044
    /**
1045
     * @return int
1046
     */
1047
    public function TotalItems()
1048
    {
1049
        return $this->Parent()->Elements()->count();
1050
    }
1051
1052
    /**
1053
     * Returns the position of the current element.
1054
     *
1055
     * @return int
1056
     */
1057
    public function Pos()
1058
    {
1059
        return ($this->Parent()->Elements()->filter('Sort:LessThan', $this->Sort)->count() + 1);
1060
    }
1061
1062
    /**
1063
     * @return string
1064
     */
1065
    public function EvenOdd()
1066
    {
1067
        $odd = (bool) ($this->Pos() % 2);
1068
1069
        return  ($odd) ? 'odd' : 'even';
1070
    }
1071
1072
    /**
1073
     * @return string
1074
     */
1075
    public static function getGraphQLTypeName(): string
1076
    {
1077
        return class_exists(StaticSchema::class)
1078
            ? StaticSchema::inst()->typeNameForDataObject(static::class)
1079
            : str_replace('\\', '_', static::class);
1080
    }
1081
}
1082