Passed
Pull Request — master (#701)
by Robbie
05:43
created

BaseElement::setDisableInlineEditing()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

888
                if ($this->/** @scrutinizer ignore-call */ isOnDraftOnly()) {
Loading history...
889
                    $data->VersionState = 'draft';
890
                    $data->VersionStateTitle = _t(
891
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.ADDEDTODRAFTHELP',
892
                        'Item has not been published yet'
893
                    );
894
                } elseif ($this->isModifiedOnDraft()) {
0 ignored issues
show
Bug introduced by
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

894
                } elseif ($this->/** @scrutinizer ignore-call */ isModifiedOnDraft()) {
Loading history...
895
                    $data->VersionState = 'modified';
896
                    $data->VersionStateTitle = $data->VersionStateTitle = _t(
897
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.MODIFIEDONDRAFTHELP',
898
                        'Item has unpublished changes'
899
                    );
900
                }
901
            }
902
903
            return $data->renderWith(__CLASS__ . '/PreviewIcon');
904
        }
905
906
        return null;
907
    }
908
909
    /**
910
     * Get a description for this content element, if available
911
     *
912
     * @return string
913
     */
914
    public function getDescription()
915
    {
916
        $description = $this->config()->uninherited('description');
917
        if ($description) {
918
            return _t(__CLASS__ . '.Description', $description);
919
        }
920
        return '';
921
    }
922
923
    /**
924
     * Generate markup for element type, with description suitable for use in
925
     * GridFields.
926
     *
927
     * @return DBField
928
     */
929
    public function getTypeNice()
930
    {
931
        $description = $this->getDescription();
932
        $desc = ($description) ? ' <span class="element__note"> &mdash; ' . $description . '</span>' : '';
933
934
        return DBField::create_field(
935
            'HTMLVarchar',
936
            $this->getType() . $desc
937
        );
938
    }
939
940
    /**
941
     * @return \SilverStripe\ORM\FieldType\DBHTMLText
942
     */
943
    public function getEditorPreview()
944
    {
945
        $templates = $this->getRenderTemplates('_EditorPreview');
946
        $templates[] = BaseElement::class . '_EditorPreview';
947
948
        return $this->renderWith($templates);
949
    }
950
951
    /**
952
     * @return Member
953
     */
954
    public function getAuthor()
955
    {
956
        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...
957
            return Member::get()->byId($this->AuthorID);
958
        }
959
960
        return null;
961
    }
962
963
    /**
964
     * Get a user defined style variant for this element, if available
965
     *
966
     * @return string
967
     */
968
    public function getStyleVariant()
969
    {
970
        $style = $this->Style;
971
        $styles = $this->config()->get('styles');
972
973
        if (isset($styles[$style])) {
974
            $style = strtolower($style);
975
        } else {
976
            $style = '';
977
        }
978
979
        $this->extend('updateStyleVariant', $style);
980
981
        return $style;
982
    }
983
984
    /**
985
     * @return mixed|null
986
     * @throws \Psr\Container\NotFoundExceptionInterface
987
     * @throws \SilverStripe\ORM\ValidationException
988
     */
989
    public function getPageTitle()
990
    {
991
        $page = $this->getPage();
992
993
        if ($page) {
994
            return $page->Title;
995
        }
996
997
        return null;
998
    }
999
1000
    /**
1001
     * @return boolean
1002
     */
1003
    public function First()
1004
    {
1005
        return ($this->Parent()->Elements()->first()->ID === $this->ID);
1006
    }
1007
1008
    /**
1009
     * @return boolean
1010
     */
1011
    public function Last()
1012
    {
1013
        return ($this->Parent()->Elements()->last()->ID === $this->ID);
1014
    }
1015
1016
    /**
1017
     * @return int
1018
     */
1019
    public function TotalItems()
1020
    {
1021
        return $this->Parent()->Elements()->count();
1022
    }
1023
1024
    /**
1025
     * Returns the position of the current element.
1026
     *
1027
     * @return int
1028
     */
1029
    public function Pos()
1030
    {
1031
        return ($this->Parent()->Elements()->filter('Sort:LessThan', $this->Sort)->count() + 1);
1032
    }
1033
1034
    /**
1035
     * @return string
1036
     */
1037
    public function EvenOdd()
1038
    {
1039
        $odd = (bool) ($this->Pos() % 2);
1040
1041
        return  ($odd) ? 'odd' : 'even';
1042
    }
1043
1044
    /**
1045
     * @param bool $disable
1046
     * @return $this
1047
     */
1048
    public function setDisableInlineEditing($disable = true)
1049
    {
1050
        $this->disableInlineEditing = (bool) $disable;
1051
        return $this;
1052
    }
1053
1054
    /**
1055
     * @return bool
1056
     */
1057
    public function getDisableInlineEditing()
1058
    {
1059
        return (bool) $this->disableInlineEditing;
1060
    }
1061
}
1062