Completed
Pull Request — master (#647)
by Robbie
20:35 queued 18:30
created

EditableFormField::getEditableFieldClasses()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 15
nc 5
nop 1
1
<?php
2
3
namespace SilverStripe\UserForms\Model;
4
5
use SilverStripe\CMS\Controllers\CMSMain;
6
use SilverStripe\CMS\Controllers\CMSPageEditController;
7
use SilverStripe\Control\Controller;
8
use SilverStripe\Core\ClassInfo;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\Core\Convert;
11
use SilverStripe\Core\Manifest\ModuleLoader;
12
use SilverStripe\Dev\Deprecation;
13
use SilverStripe\Forms\CheckboxField;
14
use SilverStripe\Forms\DropdownField;
15
use SilverStripe\Forms\FieldList;
16
use SilverStripe\Forms\GridField\GridField;
17
use SilverStripe\Forms\GridField\GridFieldButtonRow;
18
use SilverStripe\Forms\GridField\GridFieldConfig;
19
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
20
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
21
use SilverStripe\Forms\LabelField;
22
use SilverStripe\Forms\LiteralField;
23
use SilverStripe\Forms\ReadonlyField;
24
use SilverStripe\Forms\SegmentField;
25
use SilverStripe\Forms\TabSet;
26
use SilverStripe\Forms\TextField;
27
use SilverStripe\ORM\ArrayList;
28
use SilverStripe\ORM\DataObject;
29
use SilverStripe\ORM\FieldType\DBField;
30
use SilverStripe\ORM\ValidationException;
31
use SilverStripe\UserForms\Extension\UserFormFieldEditorExtension;
32
use SilverStripe\UserForms\Model\UserDefinedForm;
33
use SilverStripe\UserForms\Model\EditableCustomRule;
34
use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroup;
35
use SilverStripe\UserForms\Model\EditableFormField\EditableFieldGroupEnd;
36
use SilverStripe\UserForms\Model\EditableFormField\EditableFormStep;
37
use SilverStripe\UserForms\Model\Submission\SubmittedFormField;
38
use SilverStripe\UserForms\Modifier\DisambiguationSegmentFieldModifier;
39
use SilverStripe\UserForms\Modifier\UnderscoreSegmentFieldModifier;
40
use SilverStripe\Versioned\Versioned;
41
use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton;
42
use Symbiote\GridFieldExtensions\GridFieldEditableColumns;
43
44
/**
45
 * Represents the base class of a editable form field
46
 * object like {@link EditableTextField}.
47
 *
48
 * @package userforms
49
 *
50
 * @property string $Name
51
 * @property string $Title
52
 * @property string $Default
53
 * @property int $Sort
54
 * @property bool $Required
55
 * @property string $CustomErrorMessage
56
 * @property boolean $ShowOnLoad
57
 * @property string $DisplayRulesConjunction
58
 * @method UserDefinedForm Parent() Parent page
59
 * @method DataList DisplayRules() List of EditableCustomRule objects
60
 * @mixin Versioned
61
 */
62
class EditableFormField extends DataObject
63
{
64
    /**
65
     * Set to true to hide from class selector
66
     *
67
     * @config
68
     * @var bool
69
     */
70
    private static $hidden = false;
0 ignored issues
show
Unused Code introduced by
The property $hidden is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
71
72
    /**
73
     * Define this field as abstract (not inherited)
74
     *
75
     * @config
76
     * @var bool
77
     */
78
    private static $abstract = true;
0 ignored issues
show
Unused Code introduced by
The property $abstract is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
79
80
    /**
81
     * Flag this field type as non-data (e.g. literal, header, html)
82
     *
83
     * @config
84
     * @var bool
85
     */
86
    private static $literal = false;
0 ignored issues
show
Unused Code introduced by
The property $literal is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
87
88
    /**
89
     * Default sort order
90
     *
91
     * @config
92
     * @var string
93
     */
94
    private static $default_sort = '"Sort"';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $default_sort is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
95
96
    /**
97
     * A list of CSS classes that can be added
98
     *
99
     * @var array
100
     */
101
    public static $allowed_css = [];
102
103
    /**
104
     * Set this to true to enable placeholder field for any given class
105
     * @config
106
     * @var bool
107
     */
108
    private static $has_placeholder = false;
0 ignored issues
show
Unused Code introduced by
The property $has_placeholder is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
109
110
    /**
111
     * @config
112
     * @var array
113
     */
114
    private static $summary_fields = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $summary_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
115
        'Title'
116
    ];
117
118
    /**
119
     * @config
120
     * @var array
121
     */
122
    private static $db = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $db is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
123
        'Name' => 'Varchar',
124
        'Title' => 'Varchar(255)',
125
        'Default' => 'Varchar(255)',
126
        'Sort' => 'Int',
127
        'Required' => 'Boolean',
128
        'CustomErrorMessage' => 'Varchar(255)',
129
130
        'CustomRules' => 'Text', // @deprecated from 2.0
131
        'CustomSettings' => 'Text', // @deprecated from 2.0
132
        'Migrated' => 'Boolean', // set to true when migrated
133
134
        'ExtraClass' => 'Text', // from CustomSettings
135
        'RightTitle' => 'Varchar(255)', // from CustomSettings
136
        'ShowOnLoad' => 'Boolean(1)', // from CustomSettings
137
        'ShowInSummary' => 'Boolean',
138
        'Placeholder' => 'Varchar(255)',
139
        'DisplayRulesConjunction' => 'Enum("And,Or","Or")',
140
    ];
141
142
    private static $table_name = 'EditableFormField';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $table_name is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
143
144
145
    private static $defaults = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $defaults is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
146
        'ShowOnLoad' => true,
147
    ];
148
149
150
    /**
151
     * @config
152
     * @var array
153
     */
154
    private static $has_one = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $has_one is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
155
        'Parent' => UserDefinedForm::class,
156
    ];
157
158
    /**
159
     * Built in extensions required
160
     *
161
     * @config
162
     * @var array
163
     */
164
    private static $extensions = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $extensions is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
165
        Versioned::class . "('Stage', 'Live')"
166
    ];
167
168
    /**
169
     * @config
170
     * @var array
171
     */
172
    private static $has_many = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $has_many is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
173
        'DisplayRules' => EditableCustomRule::class . '.Parent'
174
    ];
175
176
    private static $owns = [
0 ignored issues
show
Unused Code introduced by
The property $owns is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
177
        'DisplayRules',
178
    ];
179
180
    private static $cascade_deletes = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $cascade_deletes is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
181
        'DisplayRules',
182
    ];
183
184
    /**
185
     * @var bool
186
     */
187
    protected $readonly;
188
189
    /**
190
     * Property holds the JS event which gets fired for this type of element
191
     *
192
     * @var string
193
     */
194
    protected $jsEventHandler = 'change';
195
196
    /**
197
     * Returns the jsEventHandler property for the current object. Bearing in mind it could've been overridden.
198
     * @return string
199
     */
200
    public function getJsEventHandler()
201
    {
202
        return $this->jsEventHandler;
203
    }
204
205
    /**
206
     * Set the visibility of an individual form field
207
     *
208
     * @param bool
209
     * @return $this
210
     */
211
    public function setReadonly($readonly = true)
212
    {
213
        $this->readonly = $readonly;
214
        return $this;
215
    }
216
217
    /**
218
     * Returns whether this field is readonly
219
     *
220
     * @return bool
221
     */
222
    private function isReadonly()
223
    {
224
        return $this->readonly;
225
    }
226
227
    /**
228
     * @return FieldList
229
     */
230
    public function getCMSFields()
231
    {
232
        $fields = FieldList::create(TabSet::create('Root'));
233
234
        // Main tab
235
        $fields->addFieldsToTab(
236
            'Root.Main',
237
            [
238
                ReadonlyField::create(
239
                    'Type',
240
                    _t(__CLASS__.'.TYPE', 'Type'),
241
                    $this->i18n_singular_name()
242
                ),
243
                CheckboxField::create('ShowInSummary', _t(__CLASS__.'.SHOWINSUMMARY', 'Show in summary gridfield')),
244
                LiteralField::create(
245
                    'MergeField',
246
                    _t(
247
                        __CLASS__.'.MERGEFIELDNAME',
248
                        '<div class="field readonly">' .
249
                            '<label class="left">' . _t(__CLASS__.'.MERGEFIELDNAME', 'Merge field') . '</label>' .
250
                            '<div class="middleColumn">' .
251
                                '<span class="readonly">$' . $this->Name . '</span>' .
252
                            '</div>' .
253
                        '</div>'
254
                    )
255
                ),
256
                TextField::create('Title', _t(__CLASS__.'.TITLE', 'Title')),
257
                TextField::create('Default', _t(__CLASS__.'.DEFAULT', 'Default value')),
258
                TextField::create('RightTitle', _t(__CLASS__.'.RIGHTTITLE', 'Right title')),
259
                SegmentField::create('Name', _t(__CLASS__.'.NAME', 'Name'))->setModifiers([
260
                    UnderscoreSegmentFieldModifier::create()->setDefault('FieldName'),
261
                    DisambiguationSegmentFieldModifier::create(),
262
                ])->setPreview($this->Name)
263
            ]
264
        );
265
        $fields->fieldByName('Root.Main')->setTitle(_t('SilverStripe\\CMS\\Model\\SiteTree.TABMAIN', 'Main'));
266
267
        // Custom settings
268
        if (!empty(self::$allowed_css)) {
269
            $cssList = [];
270
            foreach (self::$allowed_css as $k => $v) {
271
                if (!is_array($v)) {
272
                    $cssList[$k]=$v;
273
                } elseif ($k === $this->ClassName) {
274
                    $cssList = array_merge($cssList, $v);
275
                }
276
            }
277
278
            $fields->addFieldToTab(
279
                'Root.Main',
280
                DropdownField::create(
281
                    'ExtraClass',
282
                    _t(__CLASS__.'.EXTRACLASS_TITLE', 'Extra Styling/Layout'),
283
                    $cssList
284
                )->setDescription(_t(
285
                    __CLASS__.'.EXTRACLASS_SELECT',
286
                    'Select from the list of allowed styles'
287
                ))
288
            );
289
        } else {
290
            $fields->addFieldToTab(
291
                'Root.Main',
292
                TextField::create(
293
                    'ExtraClass',
294
                    _t(__CLASS__.'.EXTRACLASS_Title', 'Extra CSS classes')
295
                )->setDescription(_t(
296
                    __CLASS__.'.EXTRACLASS_MULTIPLE',
297
                    'Separate each CSS class with a single space'
298
                ))
299
            );
300
        }
301
302
        // Validation
303
        $validationFields = $this->getFieldValidationOptions();
304
        if ($validationFields && $validationFields->count()) {
305
            $fields->addFieldsToTab('Root.Validation', $validationFields);
0 ignored issues
show
Documentation introduced by
$validationFields is of type object<SilverStripe\Forms\FieldList>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
306
            $fields->fieldByName('Root.Validation')->setTitle(_t(__CLASS__.'.VALIDATION', 'Validation'));
307
        }
308
309
        // Add display rule fields
310
        $displayFields = $this->getDisplayRuleFields();
311
        if ($displayFields && $displayFields->count()) {
312
            $fields->addFieldsToTab('Root.DisplayRules', $displayFields);
0 ignored issues
show
Documentation introduced by
$displayFields is of type object<SilverStripe\Forms\FieldList>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
313
        }
314
315
        // Placeholder
316
        if ($this->config()->has_placeholder) {
317
            $fields->addFieldToTab(
318
                'Root.Main',
319
                TextField::create(
320
                    'Placeholder',
321
                    _t(__CLASS__.'.PLACEHOLDER', 'Placeholder')
322
                )
323
            );
324
        }
325
326
        $this->extend('updateCMSFields', $fields);
327
328
        return $fields;
329
    }
330
331
    /**
332
     * Return fields to display on the 'Display Rules' tab
333
     *
334
     * @return FieldList
335
     */
336
    protected function getDisplayRuleFields()
337
    {
338
        // Check display rules
339
        if ($this->Required) {
340
            return new FieldList(
341
                LabelField::create(
342
                    _t(
343
                        __CLASS__.'.DISPLAY_RULES_DISABLED',
344
                        'Display rules are not enabled for required fields. Please uncheck "Is this field Required?" under "Validation" to re-enable.'
345
                    )
346
                )
347
                ->addExtraClass('message warning')
348
            );
349
        }
350
351
        $allowedClasses = array_keys($this->getEditableFieldClasses(false));
352
        $editableColumns = new GridFieldEditableColumns();
353
        $editableColumns->setDisplayFields([
354
            'ConditionFieldID' => function ($record, $column, $grid) use ($allowedClasses) {
0 ignored issues
show
Unused Code introduced by
The parameter $grid is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
355
                    return DropdownField::create($column, '', EditableFormField::get()->filter([
356
                            'ParentID' => $this->ParentID,
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
357
                            'ClassName' => $allowedClasses,
358
                        ])->exclude([
359
                            'ID' => $this->ID,
360
                        ])->map('ID', 'Title'));
361
            },
362
            'ConditionOption' => function ($record, $column, $grid) {
0 ignored issues
show
Unused Code introduced by
The parameter $grid is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
363
                $options = Config::inst()->get(EditableCustomRule::class, 'condition_options');
364
365
                return DropdownField::create($column, '', $options);
366
            },
367
            'FieldValue' => function ($record, $column, $grid) {
0 ignored issues
show
Unused Code introduced by
The parameter $grid is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
368
                return TextField::create($column);
369
            }
370
        ]);
371
372
        // Custom rules
373
        $customRulesConfig = GridFieldConfig::create()
374
            ->addComponents(
375
                $editableColumns,
376
                new GridFieldButtonRow(),
377
                new GridFieldToolbarHeader(),
378
                new GridFieldAddNewInlineButton(),
379
                new GridFieldDeleteAction()
380
            );
381
382
        return new FieldList(
383
            DropdownField::create(
384
                'ShowOnLoad',
385
                _t(__CLASS__.'.INITIALVISIBILITY', 'Initial visibility'),
386
                [
387
                    1 => 'Show',
388
                    0 => 'Hide',
389
                ]
390
            ),
391
            DropdownField::create(
392
                'DisplayRulesConjunction',
393
                _t(__CLASS__.'.DISPLAYIF', 'Toggle visibility when'),
394
                [
395
                    'Or'  => _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIFOR', 'Any conditions are true'),
396
                    'And' => _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIFAND', 'All conditions are true'),
397
                ]
398
            ),
399
            GridField::create(
400
                'DisplayRules',
401
                _t(__CLASS__.'.CUSTOMRULES', 'Custom Rules'),
402
                $this->DisplayRules(),
403
                $customRulesConfig
404
            )
405
        );
406
    }
407
408
    public function onBeforeWrite()
409
    {
410
        parent::onBeforeWrite();
411
412
        // Set a field name.
413
        if (!$this->Name) {
414
            // New random name
415
            $this->Name = $this->generateName();
416
        } elseif ($this->Name === 'Field') {
417
            throw new ValidationException('Field name cannot be "Field"');
418
        }
419
420
        if (!$this->Sort && $this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
421
            $parentID = $this->ParentID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
422
            $this->Sort = EditableFormField::get()
423
                ->filter('ParentID', $parentID)
424
                ->max('Sort') + 1;
425
        }
426
    }
427
428
    /**
429
     * Generate a new non-conflicting Name value
430
     *
431
     * @return string
432
     */
433
    protected function generateName()
434
    {
435
        do {
436
            // Generate a new random name after this class (handles namespaces)
437
            $classNamePieces = explode('\\', static::class);
438
            $class = array_pop($classNamePieces);
439
            $entropy = substr(sha1(uniqid()), 0, 5);
440
            $name = "{$class}_{$entropy}";
441
442
            // Check if it conflicts
443
            $exists = EditableFormField::get()->filter('Name', $name)->count() > 0;
444
        } while ($exists);
445
        return $name;
446
    }
447
448
    /**
449
     * Flag indicating that this field will set its own error message via data-msg='' attributes
450
     *
451
     * @return bool
452
     */
453
    public function getSetsOwnError()
454
    {
455
        return false;
456
    }
457
458
    /**
459
     * Return whether a user can delete this form field
460
     * based on whether they can edit the page
461
     *
462
     * @param Member $member
463
     * @return bool
464
     */
465
    public function canDelete($member = null)
466
    {
467
        return $this->canEdit($member);
468
    }
469
470
    /**
471
     * Return whether a user can edit this form field
472
     * based on whether they can edit the page
473
     *
474
     * @param Member $member
475
     * @return bool
476
     */
477
    public function canEdit($member = null)
478
    {
479
        $parent = $this->Parent();
480
        if ($parent && $parent->exists()) {
481
            return $parent->canEdit($member) && !$this->isReadonly();
482
        } elseif (!$this->exists() && Controller::has_curr()) {
483
            // This is for GridFieldOrderableRows support as it checks edit permissions on
484
            // singleton of the class. Allows editing of User Defined Form pages by
485
            // 'Content Authors' and those with permission to edit the UDF page. (ie. CanEditType/EditorGroups)
486
            // This is to restore User Forms 2.x backwards compatibility.
487
            $controller = Controller::curr();
488
            if ($controller && $controller instanceof CMSPageEditController) {
489
                $parent = $controller->getRecord($controller->currentPageID());
490
                // Only allow this behaviour on pages using UserFormFieldEditorExtension, such
491
                // as UserDefinedForm page type.
492
                if ($parent && $parent->hasExtension(UserFormFieldEditorExtension::class)) {
493
                    return $parent->canEdit($member);
494
                }
495
            }
496
        }
497
498
        // Fallback to secure admin permissions
499
        return parent::canEdit($member);
0 ignored issues
show
Bug Compatibility introduced by
The expression parent::canEdit($member); of type boolean|string adds the type string to the return on line 499 which is incompatible with the return type documented by SilverStripe\UserForms\M...tableFormField::canEdit of type boolean.
Loading history...
500
    }
501
502
    /**
503
     * Return whether a user can view this form field
504
     * based on whether they can view the page, regardless of the ReadOnly status of the field
505
     *
506
     * @param Member $member
507
     * @return bool
508
     */
509
    public function canView($member = null)
510
    {
511
        $parent = $this->Parent();
512
        if ($parent && $parent->exists()) {
513
            return $parent->canView($member);
514
        }
515
516
        return true;
517
    }
518
519
    /**
520
     * Return whether a user can create an object of this type
521
     *
522
     * @param Member $member
523
     * @param array $context Virtual parameter to allow context to be passed in to check
524
     * @return bool
525
     */
526 View Code Duplication
    public function canCreate($member = null, $context = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
527
    {
528
        // Check parent page
529
        $parent = $this->getCanCreateContext(func_get_args());
530
        if ($parent) {
531
            return $parent->canEdit($member);
532
        }
533
534
        // Fall back to secure admin permissions
535
        return parent::canCreate($member);
0 ignored issues
show
Bug Compatibility introduced by
The expression parent::canCreate($member); of type boolean|string adds the type string to the return on line 535 which is incompatible with the return type documented by SilverStripe\UserForms\M...bleFormField::canCreate of type boolean.
Loading history...
536
    }
537
538
    /**
539
     * Helper method to check the parent for this object
540
     *
541
     * @param array $args List of arguments passed to canCreate
542
     * @return SiteTree Parent page instance
543
     */
544 View Code Duplication
    protected function getCanCreateContext($args)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
545
    {
546
        // Inspect second parameter to canCreate for a 'Parent' context
547
        if (isset($args[1]['Parent'])) {
548
            return $args[1]['Parent'];
549
        }
550
        // Hack in currently edited page if context is missing
551
        if (Controller::has_curr() && Controller::curr() instanceof CMSMain) {
552
            return Controller::curr()->currentPage();
553
        }
554
555
        // No page being edited
556
        return null;
557
    }
558
559
    /**
560
     * checks whether record is new, copied from SiteTree
561
     */
562
    public function isNew()
563
    {
564
        if (empty($this->ID)) {
565
            return true;
566
        }
567
568
        if (is_numeric($this->ID)) {
569
            return false;
570
        }
571
572
        return stripos($this->ID, 'new') === 0;
573
    }
574
575
    /**
576
     * @deprecated since version 4.0
577
     */
578
    public function getSettings()
579
    {
580
        Deprecation::notice('4.0', 'getSettings is deprecated');
581
        return (!empty($this->CustomSettings)) ? unserialize($this->CustomSettings) : array();
0 ignored issues
show
Documentation introduced by
The property CustomSettings does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
582
    }
583
584
    /**
585
     * @deprecated since version 4.0
586
     */
587
    public function setSettings($settings = array())
588
    {
589
        Deprecation::notice('4.0', 'setSettings is deprecated');
590
        $this->CustomSettings = serialize($settings);
0 ignored issues
show
Documentation introduced by
The property CustomSettings does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
591
    }
592
593
    /**
594
     * @deprecated since version 4.0
595
     */
596
    public function setSetting($key, $value)
597
    {
598
        Deprecation::notice('4.0', "setSetting({$key}) is deprecated");
599
        $settings = $this->getSettings();
0 ignored issues
show
Deprecated Code introduced by
The method SilverStripe\UserForms\M...ormField::getSettings() has been deprecated with message: since version 4.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
600
        $settings[$key] = $value;
601
602
        $this->setSettings($settings);
0 ignored issues
show
Deprecated Code introduced by
The method SilverStripe\UserForms\M...ormField::setSettings() has been deprecated with message: since version 4.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
603
    }
604
605
    /**
606
     * Set the allowed css classes for the extraClass custom setting
607
     *
608
     * @param array $allowed The permissible CSS classes to add
609
     */
610
    public function setAllowedCss(array $allowed)
611
    {
612
        if (is_array($allowed)) {
613
            foreach ($allowed as $k => $v) {
614
                self::$allowed_css[$k] = (!is_null($v)) ? $v : $k;
615
            }
616
        }
617
    }
618
619
    /**
620
     * @deprecated since version 4.0
621
     */
622
    public function getSetting($setting)
623
    {
624
        Deprecation::notice("4.0", "getSetting({$setting}) is deprecated");
625
626
        $settings = $this->getSettings();
0 ignored issues
show
Deprecated Code introduced by
The method SilverStripe\UserForms\M...ormField::getSettings() has been deprecated with message: since version 4.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
627
        if (isset($settings) && count($settings) > 0) {
628
            if (isset($settings[$setting])) {
629
                return $settings[$setting];
630
            }
631
        }
632
        return '';
633
    }
634
635
    /**
636
     * Get the path to the icon for this field type, relative to the site root.
637
     *
638
     * @return string
639
     */
640
    public function getIcon()
641
    {
642
        return ModuleLoader::getModule('silverstripe/userforms')
643
            ->getRelativeResourcePath('images/' . strtolower($this->class) . '.png');
0 ignored issues
show
Documentation introduced by
The property class does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
644
    }
645
646
    /**
647
     * Return whether or not this field has addable options
648
     * such as a dropdown field or radio set
649
     *
650
     * @return bool
651
     */
652
    public function getHasAddableOptions()
653
    {
654
        return false;
655
    }
656
657
    /**
658
     * Return whether or not this field needs to show the extra
659
     * options dropdown list
660
     *
661
     * @return bool
662
     */
663
    public function showExtraOptions()
664
    {
665
        return true;
666
    }
667
668
    /**
669
     * Returns the Title for rendering in the front-end (with XML values escaped)
670
     *
671
     * @return string
672
     */
673
    public function getEscapedTitle()
674
    {
675
        return Convert::raw2xml($this->Title);
676
    }
677
678
    /**
679
     * Find the numeric indicator (1.1.2) that represents it's nesting value
680
     *
681
     * Only useful for fields attached to a current page, and that contain other fields such as pages
682
     * or groups
683
     *
684
     * @return string
685
     */
686
    public function getFieldNumber()
687
    {
688
        // Check if exists
689
        if (!$this->exists()) {
690
            return null;
691
        }
692
        // Check parent
693
        $form = $this->Parent();
694
        if (!$form || !$form->exists() || !($fields = $form->Fields())) {
695
            return null;
696
        }
697
698
        $prior = 0; // Number of prior group at this level
699
        $stack = []; // Current stack of nested groups, where the top level = the page
700
        foreach ($fields->map('ID', 'ClassName') as $id => $className) {
701
            if ($className === EditableFormStep::class) {
702
                $priorPage = empty($stack) ? $prior : $stack[0];
703
                $stack = array($priorPage + 1);
704
                $prior = 0;
705
            } elseif ($className === EditableFieldGroup::class) {
706
                $stack[] = $prior + 1;
707
                $prior = 0;
708
            } elseif ($className === EditableFieldGroupEnd::class) {
709
                $prior = array_pop($stack);
710
            }
711
            if ($id == $this->ID) {
712
                return implode('.', $stack);
713
            }
714
        }
715
        return null;
716
    }
717
718
    public function getCMSTitle()
719
    {
720
        return $this->i18n_singular_name() . ' (' . $this->Title . ')';
721
    }
722
723
    /**
724
     * @deprecated since version 4.0
725
     */
726
    public function getFieldName($field = false)
727
    {
728
        Deprecation::notice('4.0', "getFieldName({$field}) is deprecated");
729
        return ($field) ? "Fields[".$this->ID."][".$field."]" : "Fields[".$this->ID."]";
730
    }
731
732
    /**
733
     * @deprecated since version 4.0
734
     */
735
    public function getSettingName($field)
736
    {
737
        Deprecation::notice('4.0', "getSettingName({$field}) is deprecated");
738
        $name = $this->getFieldName('CustomSettings');
0 ignored issues
show
Documentation introduced by
'CustomSettings' is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Deprecated Code introduced by
The method SilverStripe\UserForms\M...rmField::getFieldName() has been deprecated with message: since version 4.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
739
740
        return $name . '[' . $field .']';
741
    }
742
743
    /**
744
     * Append custom validation fields to the default 'Validation'
745
     * section in the editable options view
746
     *
747
     * @return FieldList
748
     */
749
    public function getFieldValidationOptions()
750
    {
751
        $fields = new FieldList(
752
            CheckboxField::create('Required', _t(__CLASS__.'.REQUIRED', 'Is this field Required?'))
753
                ->setDescription(_t(__CLASS__.'.REQUIRED_DESCRIPTION', 'Please note that conditional fields can\'t be required')),
754
            TextField::create('CustomErrorMessage', _t(__CLASS__.'.CUSTOMERROR', 'Custom Error Message'))
755
        );
756
757
        $this->extend('updateFieldValidationOptions', $fields);
758
759
        return $fields;
760
    }
761
762
    /**
763
     * Return a FormField to appear on the front end. Implement on
764
     * your subclass.
765
     *
766
     * @return FormField
767
     */
768
    public function getFormField()
769
    {
770
        user_error("Please implement a getFormField() on your EditableFormClass ". $this->ClassName, E_USER_ERROR);
771
    }
772
773
    /**
774
     * Updates a formfield with extensions
775
     *
776
     * @param FormField $field
777
     */
778
    public function doUpdateFormField($field)
779
    {
780
        $this->extend('beforeUpdateFormField', $field);
781
        $this->updateFormField($field);
782
        $this->extend('afterUpdateFormField', $field);
783
    }
784
785
    /**
786
     * Updates a formfield with the additional metadata specified by this field
787
     *
788
     * @param FormField $field
789
     */
790
    protected function updateFormField($field)
791
    {
792
        // set the error / formatting messages
793
        $field->setCustomValidationMessage($this->getErrorMessage()->RAW());
794
795
        // set the right title on this field
796
        if ($this->RightTitle) {
0 ignored issues
show
Documentation introduced by
The property RightTitle does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
797
            // Since this field expects raw html, safely escape the user data prior
798
            $field->setRightTitle(Convert::raw2xml($this->RightTitle));
0 ignored issues
show
Documentation introduced by
The property RightTitle does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
799
        }
800
801
        // if this field is required add some
802
        if ($this->Required) {
803
            // Required validation can conflict so add the Required validation messages as input attributes
804
            $errorMessage = $this->getErrorMessage()->HTML();
805
            $field->addExtraClass('requiredField');
806
            $field->setAttribute('data-rule-required', 'true');
807
            $field->setAttribute('data-msg-required', $errorMessage);
808
809
            if ($identifier = UserDefinedForm::config()->required_identifier) {
810
                $title = $field->Title() . " <span class='required-identifier'>". $identifier . "</span>";
811
                $field->setTitle($title);
812
            }
813
        }
814
815
        // if this field has an extra class
816
        if ($this->ExtraClass) {
0 ignored issues
show
Documentation introduced by
The property ExtraClass does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
817
            $field->addExtraClass($this->ExtraClass);
0 ignored issues
show
Documentation introduced by
The property ExtraClass does not exist on object<SilverStripe\User...odel\EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
818
        }
819
820
        // if ShowOnLoad is false hide the field
821
        if (!$this->ShowOnLoad) {
822
            $field->addExtraClass($this->ShowOnLoadNice());
823
        }
824
825
        // if this field has a placeholder
826
        if ($this->Placeholder) {
0 ignored issues
show
Bug introduced by
The property Placeholder does not seem to exist. Did you mean has_placeholder?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
827
            $field->setAttribute('placeholder', $this->Placeholder);
0 ignored issues
show
Bug introduced by
The property Placeholder does not seem to exist. Did you mean has_placeholder?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
828
        }
829
    }
830
831
    /**
832
     * Return the instance of the submission field class
833
     *
834
     * @return SubmittedFormField
835
     */
836
    public function getSubmittedFormField()
837
    {
838
        return SubmittedFormField::create();
839
    }
840
841
842
    /**
843
     * Show this form field (and its related value) in the reports and in emails.
844
     *
845
     * @return bool
846
     */
847
    public function showInReports()
848
    {
849
        return true;
850
    }
851
852
    /**
853
     * Return the error message for this field. Either uses the custom
854
     * one (if provided) or the default SilverStripe message
855
     *
856
     * @return Varchar
857
     */
858
    public function getErrorMessage()
859
    {
860
        $title = strip_tags("'". ($this->Title ? $this->Title : $this->Name) . "'");
861
        $standard = _t('SilverStripe\\Forms\\Form.FIELDISREQUIRED', '{field} is required.', ['field' => $title]);
862
863
        // only use CustomErrorMessage if it has a non empty value
864
        $errorMessage = (!empty($this->CustomErrorMessage)) ? $this->CustomErrorMessage : $standard;
865
866
        return DBField::create_field('Varchar', $errorMessage);
867
    }
868
869
    /**
870
     * Invoked by UserFormUpgradeService to migrate settings specific to this field from CustomSettings
871
     * to the field proper
872
     *
873
     * @param array $data Unserialised data
874
     */
875
    public function migrateSettings($data)
876
    {
877
        // Map 'Show' / 'Hide' to boolean
878
        if (isset($data['ShowOnLoad'])) {
879
            $this->ShowOnLoad = $data['ShowOnLoad'] === '' || ($data['ShowOnLoad'] && $data['ShowOnLoad'] !== 'Hide');
880
            unset($data['ShowOnLoad']);
881
        }
882
883
        // Migrate all other settings
884
        foreach ($data as $key => $value) {
885
            if ($this->hasField($key)) {
886
                $this->setField($key, $value);
887
            }
888
        }
889
    }
890
891
    /**
892
     * Get the formfield to use when editing this inline in gridfield
893
     *
894
     * @param string $column name of column
895
     * @param array $fieldClasses List of allowed classnames if this formfield has a selectable class
896
     * @return FormField
897
     */
898
    public function getInlineClassnameField($column, $fieldClasses)
899
    {
900
        return DropdownField::create($column, false, $fieldClasses);
901
    }
902
903
    /**
904
     * Get the formfield to use when editing the title inline
905
     *
906
     * @param string $column
907
     * @return FormField
908
     */
909
    public function getInlineTitleField($column)
910
    {
911
        return TextField::create($column, false)
912
            ->setAttribute('placeholder', _t(__CLASS__.'.TITLE', 'Title'))
913
            ->setAttribute('data-placeholder', _t(__CLASS__.'.TITLE', 'Title'));
914
    }
915
916
    /**
917
     * Get the JS expression for selecting the holder for this field
918
     *
919
     * @return string
920
     */
921
    public function getSelectorHolder()
922
    {
923
        return sprintf('$("%s")', $this->getSelectorOnly());
924
    }
925
926
    /**
927
     * Returns only the JS identifier of a string, less the $(), which can be inserted elsewhere, for example when you
928
     * want to perform selections on multiple selectors
929
     * @return string
930
     */
931
    public function getSelectorOnly()
932
    {
933
        return "#{$this->Name}";
934
    }
935
936
    /**
937
     * Gets the JS expression for selecting the value for this field
938
     *
939
     * @param EditableCustomRule $rule Custom rule this selector will be used with
940
     * @param bool $forOnLoad Set to true if this will be invoked on load
941
     *
942
     * @return string
943
     */
944
    public function getSelectorField(EditableCustomRule $rule, $forOnLoad = false)
945
    {
946
        return sprintf("$(%s)", $this->getSelectorFieldOnly());
947
    }
948
949
    /**
950
     * @return string
951
     */
952
    public function getSelectorFieldOnly()
953
    {
954
        return "[name='{$this->Name}']";
955
    }
956
957
958
    /**
959
     * Get the list of classes that can be selected and used as data-values
960
     *
961
     * @param $includeLiterals Set to false to exclude non-data fields
962
     * @return array
963
     */
964
    public function getEditableFieldClasses($includeLiterals = true)
965
    {
966
        $classes = ClassInfo::getValidSubClasses(EditableFormField::class);
967
968
        // Remove classes we don't want to display in the dropdown.
969
        $editableFieldClasses = [];
970
        foreach ($classes as $class) {
971
            // Skip abstract / hidden classes
972
            if (Config::inst()->get($class, 'abstract', Config::UNINHERITED)
973
                || Config::inst()->get($class, 'hidden')
974
            ) {
975
                continue;
976
            }
977
978
            if (!$includeLiterals && Config::inst()->get($class, 'literal')) {
979
                continue;
980
            }
981
982
            $singleton = singleton($class);
983
            if (!$singleton->canCreate()) {
984
                continue;
985
            }
986
987
            $editableFieldClasses[$class] = $singleton->i18n_singular_name();
988
        }
989
990
        asort($editableFieldClasses);
991
        return $editableFieldClasses;
992
    }
993
994
    /**
995
     * @return EditableFormFieldValidator
996
     */
997
    public function getCMSValidator()
998
    {
999
        return EditableFormFieldValidator::create()
1000
            ->setRecord($this);
1001
    }
1002
1003
    /**
1004
     * Determine effective display rules for this field.
1005
     *
1006
     * @return SS_List
1007
     */
1008
    public function EffectiveDisplayRules()
1009
    {
1010
        if ($this->Required) {
1011
            return ArrayList::create();
1012
        }
1013
        return $this->DisplayRules();
1014
    }
1015
1016
    /**
1017
     * Extracts info from DisplayRules into array so UserDefinedForm->buildWatchJS can run through it.
1018
     * @return array|null
1019
     */
1020
    public function formatDisplayRules()
1021
    {
1022
        $holderSelector = $this->getSelectorOnly();
1023
        $result = [
1024
            'targetFieldID' => $holderSelector,
1025
            'conjunction'   => $this->DisplayRulesConjunctionNice(),
1026
            'selectors'     => [],
1027
            'events'        => [],
1028
            'operations'    => [],
1029
            'initialState'  => $this->ShowOnLoadNice(),
1030
            'view'          => [],
1031
            'opposite'      => [],
1032
        ];
1033
1034
        // Check for field dependencies / default
1035
        /** @var EditableCustomRule $rule */
1036
        foreach ($this->EffectiveDisplayRules() as $rule) {
1037
            // Get the field which is effected
1038
            /** @var EditableFormField $formFieldWatch */
1039
            $formFieldWatch = DataObject::get_by_id(EditableFormField::class, $rule->ConditionFieldID);
1040
            // Skip deleted fields
1041
            if (! $formFieldWatch) {
1042
                continue;
1043
            }
1044
            $fieldToWatch = $formFieldWatch->getSelectorFieldOnly();
1045
1046
            $expression = $rule->buildExpression();
1047
            if (!in_array($fieldToWatch, $result['selectors'])) {
1048
                $result['selectors'][] = $fieldToWatch;
1049
            }
1050
            if (!in_array($expression['event'], $result['events'])) {
1051
                $result['events'][] = $expression['event'];
1052
            }
1053
            $result['operations'][] = $expression['operation'];
1054
1055
            // View/Show should read
1056
            $opposite = ($result['initialState'] === 'hide') ? 'show' : 'hide';
1057
            $result['view'] = $rule->toggleDisplayText($result['initialState']);
1058
            $result['opposite'] = $rule->toggleDisplayText($opposite);
1059
        }
1060
1061
        return (count($result['selectors'])) ? $result : null;
1062
    }
1063
1064
    /**
1065
     * Replaces the set DisplayRulesConjunction with their JS logical operators
1066
     * @return string
1067
     */
1068
    public function DisplayRulesConjunctionNice()
1069
    {
1070
        return (strtolower($this->DisplayRulesConjunction) === 'or') ? '||' : '&&';
1071
    }
1072
1073
    /**
1074
     * Replaces boolean ShowOnLoad with its JS string equivalent
1075
     * @return string
1076
     */
1077
    public function ShowOnLoadNice()
1078
    {
1079
        return ($this->ShowOnLoad) ? 'show' : 'hide';
1080
    }
1081
1082
    /**
1083
     * Returns whether this is of type EditableCheckBoxField
1084
     * @return bool
1085
     */
1086
    public function isCheckBoxField()
1087
    {
1088
        return false;
1089
    }
1090
1091
    /**
1092
     * Returns whether this is of type EditableRadioField
1093
     * @return bool
1094
     */
1095
    public function isRadioField()
1096
    {
1097
        return false;
1098
    }
1099
1100
    /**
1101
     * Determined is this is of type EditableCheckboxGroupField
1102
     * @return bool
1103
     */
1104
    public function isCheckBoxGroupField()
1105
    {
1106
        return false;
1107
    }
1108
}
1109