1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Drupal\entity_browser\Plugin\Field\FieldWidget; |
4
|
|
|
|
5
|
|
|
use Drupal\Core\Entity\EntityInterface; |
6
|
|
|
use Drupal\entity_browser\Element\EntityBrowserElement; |
7
|
|
|
use Symfony\Component\Validator\ConstraintViolationInterface; |
8
|
|
|
use Drupal\Component\Utility\Html; |
9
|
|
|
use Drupal\Component\Utility\NestedArray; |
10
|
|
|
use Drupal\Core\Entity\ContentEntityInterface; |
11
|
|
|
use Drupal\Core\Entity\EntityTypeManagerInterface; |
12
|
|
|
use Drupal\Core\Field\FieldDefinitionInterface; |
13
|
|
|
use Drupal\Core\Field\FieldItemListInterface; |
14
|
|
|
use Drupal\Core\Field\WidgetBase; |
15
|
|
|
use Drupal\Core\Form\FormStateInterface; |
16
|
|
|
use Drupal\Core\Plugin\ContainerFactoryPluginInterface; |
17
|
|
|
use Drupal\Core\Url; |
18
|
|
|
use Drupal\Core\Validation\Plugin\Validation\Constraint\NotNullConstraint; |
19
|
|
|
use Drupal\entity_browser\FieldWidgetDisplayManager; |
20
|
|
|
use Symfony\Component\DependencyInjection\ContainerInterface; |
21
|
|
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface; |
22
|
|
|
use Symfony\Component\Validator\ConstraintViolation; |
23
|
|
|
use Symfony\Component\Validator\ConstraintViolationListInterface; |
24
|
|
|
use Drupal\Core\Extension\ModuleHandlerInterface; |
25
|
|
|
use Drupal\Core\Session\AccountInterface; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Plugin implementation of the 'entity_reference' widget for entity browser. |
29
|
|
|
* |
30
|
|
|
* @FieldWidget( |
31
|
|
|
* id = "entity_browser_entity_reference", |
32
|
|
|
* label = @Translation("Entity browser"), |
33
|
|
|
* description = @Translation("Uses entity browser to select entities."), |
34
|
|
|
* multiple_values = TRUE, |
35
|
|
|
* field_types = { |
36
|
|
|
* "entity_reference" |
37
|
|
|
* } |
38
|
|
|
* ) |
39
|
|
|
*/ |
40
|
|
|
class EntityReferenceBrowserWidget extends WidgetBase implements ContainerFactoryPluginInterface { |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Entity type manager service. |
44
|
|
|
* |
45
|
|
|
* @var \Drupal\Core\Entity\EntityTypeManagerInterface |
46
|
|
|
*/ |
47
|
|
|
protected $entityTypeManager; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Field widget display plugin manager. |
51
|
|
|
* |
52
|
|
|
* @var \Drupal\entity_browser\FieldWidgetDisplayManager |
53
|
|
|
*/ |
54
|
|
|
protected $fieldDisplayManager; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* The depth of the delete button. |
58
|
|
|
* |
59
|
|
|
* This property exists so it can be changed if subclasses. |
60
|
|
|
* |
61
|
|
|
* @var int |
62
|
|
|
*/ |
63
|
|
|
protected static $deleteDepth = 4; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* The module handler interface. |
67
|
|
|
* |
68
|
|
|
* @var \Drupal\Core\Extension\ModuleHandlerInterface |
69
|
|
|
*/ |
70
|
|
|
protected $moduleHandler; |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* The current user. |
74
|
|
|
* |
75
|
|
|
* @var \Drupal\Core\Session\AccountInterface |
76
|
|
|
*/ |
77
|
|
|
protected $currentUser; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Constructs widget plugin. |
81
|
|
|
* |
82
|
|
|
* @param string $plugin_id |
83
|
|
|
* The plugin_id for the plugin instance. |
84
|
|
|
* @param mixed $plugin_definition |
85
|
|
|
* The plugin implementation definition. |
86
|
|
|
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition |
87
|
|
|
* The definition of the field to which the widget is associated. |
88
|
|
|
* @param array $settings |
89
|
|
|
* The widget settings. |
90
|
|
|
* @param array $third_party_settings |
91
|
|
|
* Any third party settings. |
92
|
|
|
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager |
93
|
|
|
* Entity type manager service. |
94
|
|
|
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher |
95
|
|
|
* Event dispatcher. |
96
|
|
|
* @param \Drupal\entity_browser\FieldWidgetDisplayManager $field_display_manager |
97
|
|
|
* Field widget display plugin manager. |
98
|
|
|
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler |
99
|
|
|
* The module handler service. |
100
|
|
|
* @param \Drupal\Core\Session\AccountInterface $current_user |
101
|
|
|
* The current user. |
102
|
|
|
*/ |
103
|
|
|
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher, FieldWidgetDisplayManager $field_display_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user) { |
104
|
|
|
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); |
105
|
|
|
$this->entityTypeManager = $entity_type_manager; |
106
|
|
|
$this->fieldDisplayManager = $field_display_manager; |
107
|
|
|
$this->moduleHandler = $module_handler; |
108
|
|
|
$this->currentUser = $current_user; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* {@inheritdoc} |
113
|
|
|
*/ |
114
|
|
|
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { |
115
|
|
|
return new static( |
116
|
|
|
$plugin_id, |
117
|
|
|
$plugin_definition, |
118
|
|
|
$configuration['field_definition'], |
119
|
|
|
$configuration['settings'], |
120
|
|
|
$configuration['third_party_settings'], |
121
|
|
|
$container->get('entity_type.manager'), |
122
|
|
|
$container->get('event_dispatcher'), |
123
|
|
|
$container->get('plugin.manager.entity_browser.field_widget_display'), |
124
|
|
|
$container->get('module_handler'), |
125
|
|
|
$container->get('current_user') |
126
|
|
|
); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* {@inheritdoc} |
131
|
|
|
*/ |
132
|
|
|
public static function defaultSettings() { |
133
|
|
|
return [ |
134
|
|
|
'entity_browser' => NULL, |
135
|
|
|
'open' => FALSE, |
136
|
|
|
'field_widget_display' => 'label', |
137
|
|
|
'field_widget_edit' => TRUE, |
138
|
|
|
'field_widget_remove' => TRUE, |
139
|
|
|
'field_widget_display_settings' => [], |
140
|
|
|
'selection_mode' => EntityBrowserElement::SELECTION_MODE_APPEND, |
141
|
|
|
] + parent::defaultSettings(); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* {@inheritdoc} |
146
|
|
|
*/ |
147
|
|
|
public function settingsForm(array $form, FormStateInterface $form_state) { |
148
|
|
|
$element = parent::settingsForm($form, $form_state); |
149
|
|
|
|
150
|
|
|
$browsers = []; |
151
|
|
|
/** @var \Drupal\entity_browser\EntityBrowserInterface $browser */ |
152
|
|
|
foreach ($this->entityTypeManager->getStorage('entity_browser')->loadMultiple() as $browser) { |
153
|
|
|
$browsers[$browser->id()] = $browser->label(); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
$element['entity_browser'] = [ |
157
|
|
|
'#title' => $this->t('Entity browser'), |
158
|
|
|
'#type' => 'select', |
159
|
|
|
'#default_value' => $this->getSetting('entity_browser'), |
160
|
|
|
'#options' => $browsers, |
161
|
|
|
]; |
162
|
|
|
|
163
|
|
|
$target_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type'); |
164
|
|
|
$entity_type = $this->entityTypeManager->getStorage($target_type)->getEntityType(); |
165
|
|
|
|
166
|
|
|
$displays = []; |
167
|
|
|
foreach ($this->fieldDisplayManager->getDefinitions() as $id => $definition) { |
168
|
|
|
if ($this->fieldDisplayManager->createInstance($id)->isApplicable($entity_type)) { |
169
|
|
|
$displays[$id] = $definition['label']; |
170
|
|
|
} |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
$id = Html::getUniqueId('field-' . $this->fieldDefinition->getName() . '-display-settings-wrapper'); |
174
|
|
|
$element['field_widget_display'] = [ |
175
|
|
|
'#title' => $this->t('Entity display plugin'), |
176
|
|
|
'#type' => 'select', |
177
|
|
|
'#default_value' => $this->getSetting('field_widget_display'), |
178
|
|
|
'#options' => $displays, |
179
|
|
|
'#ajax' => [ |
180
|
|
|
'callback' => [$this, 'updateSettingsAjax'], |
181
|
|
|
'wrapper' => $id, |
182
|
|
|
], |
183
|
|
|
]; |
184
|
|
|
|
185
|
|
|
$edit_button_access = TRUE; |
186
|
|
|
if ($entity_type->id() == 'file') { |
187
|
|
|
// For entities of type "file", it only makes sense to have the edit |
188
|
|
|
// button if the module "file_entity" is present. |
189
|
|
|
$edit_button_access = $this->moduleHandler->moduleExists('file_entity'); |
190
|
|
|
} |
191
|
|
|
$element['field_widget_edit'] = [ |
192
|
|
|
'#title' => $this->t('Display Edit button'), |
193
|
|
|
'#type' => 'checkbox', |
194
|
|
|
'#default_value' => $this->getSetting('field_widget_edit'), |
195
|
|
|
'#access' => $edit_button_access, |
196
|
|
|
]; |
197
|
|
|
|
198
|
|
|
$element['field_widget_remove'] = [ |
199
|
|
|
'#title' => $this->t('Display Remove button'), |
200
|
|
|
'#type' => 'checkbox', |
201
|
|
|
'#default_value' => $this->getSetting('field_widget_remove'), |
202
|
|
|
]; |
203
|
|
|
|
204
|
|
|
$element['open'] = [ |
205
|
|
|
'#title' => $this->t('Show widget details as open by default'), |
206
|
|
|
'#description' => $this->t('If marked, the fieldset container that wraps the browser on the entity form will be loaded initially expanded.'), |
207
|
|
|
'#type' => 'checkbox', |
208
|
|
|
'#default_value' => $this->getSetting('open'), |
209
|
|
|
]; |
210
|
|
|
|
211
|
|
|
$element['selection_mode'] = [ |
212
|
|
|
'#title' => $this->t('Selection mode'), |
213
|
|
|
'#description' => $this->t('Determines how selection in entity browser will be handled. Will selection be appended/prepended or it will be replaced in case of editing.'), |
214
|
|
|
'#type' => 'select', |
215
|
|
|
'#options' => EntityBrowserElement::getSelectionModeOptions(), |
216
|
|
|
'#default_value' => $this->getSetting('selection_mode'), |
217
|
|
|
]; |
218
|
|
|
|
219
|
|
|
$element['field_widget_display_settings'] = [ |
220
|
|
|
'#type' => 'fieldset', |
221
|
|
|
'#title' => $this->t('Entity display plugin configuration'), |
222
|
|
|
'#tree' => TRUE, |
223
|
|
|
'#prefix' => '<div id="' . $id . '">', |
224
|
|
|
'#suffix' => '</div>', |
225
|
|
|
]; |
226
|
|
|
|
227
|
|
|
if ($this->getSetting('field_widget_display')) { |
228
|
|
|
$element['field_widget_display_settings'] += $this->fieldDisplayManager |
229
|
|
|
->createInstance( |
230
|
|
|
$form_state->getValue( |
231
|
|
|
['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display'], |
232
|
|
|
$this->getSetting('field_widget_display') |
233
|
|
|
), |
234
|
|
|
$form_state->getValue( |
235
|
|
|
['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display_settings'], |
236
|
|
|
$this->getSetting('field_widget_display_settings') |
237
|
|
|
) + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')] |
238
|
|
|
) |
239
|
|
|
->settingsForm($form, $form_state); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
return $element; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Ajax callback that updates field widget display settings fieldset. |
247
|
|
|
*/ |
248
|
|
|
public function updateSettingsAjax(array $form, FormStateInterface $form_state) { |
249
|
|
|
return $form['fields'][$this->fieldDefinition->getName()]['plugin']['settings_edit_form']['settings']['field_widget_display_settings']; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* {@inheritdoc} |
254
|
|
|
*/ |
255
|
|
|
public function settingsSummary() { |
256
|
|
|
$summary = $this->summaryBase(); |
257
|
|
|
$field_widget_display = $this->getSetting('field_widget_display'); |
258
|
|
|
|
259
|
|
|
if (!empty($field_widget_display)) { |
260
|
|
|
$plugin = $this->fieldDisplayManager->getDefinition($field_widget_display); |
261
|
|
|
$summary[] = $this->t('Entity display: @name', ['@name' => $plugin['label']]); |
262
|
|
|
} |
263
|
|
|
return $summary; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* {@inheritdoc} |
268
|
|
|
*/ |
269
|
|
|
public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) { |
270
|
|
|
if ($violations->count() > 0) { |
271
|
|
|
/** @var \Symfony\Component\Validator\ConstraintViolation $violation */ |
272
|
|
|
foreach ($violations as $offset => $violation) { |
273
|
|
|
// The value of the required field is checked through the "not null" |
274
|
|
|
// constraint, whose message is not very useful. We override it here for |
275
|
|
|
// better UX. |
276
|
|
|
if ($violation->getConstraint() instanceof NotNullConstraint) { |
|
|
|
|
277
|
|
|
$violations->set($offset, new ConstraintViolation( |
278
|
|
|
$this->t('@name field is required.', ['@name' => $items->getFieldDefinition()->getLabel()]), |
279
|
|
|
'', |
280
|
|
|
[], |
281
|
|
|
$violation->getRoot(), |
282
|
|
|
$violation->getPropertyPath(), |
283
|
|
|
$violation->getInvalidValue(), |
284
|
|
|
$violation->getPlural(), |
285
|
|
|
$violation->getCode(), |
286
|
|
|
$violation->getConstraint(), |
287
|
|
|
$violation->getCause() |
288
|
|
|
)); |
289
|
|
|
} |
290
|
|
|
} |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
parent::flagErrors($items, $violations, $form, $form_state); |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Returns a key used to store the previously loaded entity. |
298
|
|
|
* |
299
|
|
|
* @param \Drupal\Core\Field\FieldItemListInterface $items |
300
|
|
|
* The field items. |
301
|
|
|
* |
302
|
|
|
* @return string |
303
|
|
|
* A key for form state storage. |
304
|
|
|
*/ |
305
|
|
|
protected function getFormStateKey(FieldItemListInterface $items) { |
306
|
|
|
return $items->getEntity()->uuid() . ':' . $items->getFieldDefinition()->getName(); |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* {@inheritdoc} |
311
|
|
|
*/ |
312
|
|
|
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { |
313
|
|
|
$entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type'); |
|
|
|
|
314
|
|
|
$entities = $this->formElementEntities($items, $element, $form_state); |
315
|
|
|
|
316
|
|
|
// Get correct ordered list of entity IDs. |
317
|
|
|
$ids = array_map( |
318
|
|
|
function (EntityInterface $entity) { |
319
|
|
|
return $entity->id(); |
320
|
|
|
}, |
321
|
|
|
$entities |
322
|
|
|
); |
323
|
|
|
|
324
|
|
|
// We store current entity IDs as we might need them in future requests. If |
325
|
|
|
// some other part of the form triggers an AJAX request with |
326
|
|
|
// #limit_validation_errors we won't have access to the value of the |
327
|
|
|
// target_id element and won't be able to build the form as a result of |
328
|
|
|
// that. This will cause missing submit (Remove, Edit, ...) elements, which |
329
|
|
|
// might result in unpredictable results. |
330
|
|
|
$form_state->set(['entity_browser_widget', $this->getFormStateKey($items)], $ids); |
331
|
|
|
|
332
|
|
|
$hidden_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName() . '-target-id'); |
333
|
|
|
$details_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName()); |
334
|
|
|
|
335
|
|
|
$element += [ |
336
|
|
|
'#id' => $details_id, |
337
|
|
|
'#type' => 'details', |
338
|
|
|
'#open' => !empty($entities) || $this->getSetting('open'), |
339
|
|
|
'#required' => $this->fieldDefinition->isRequired(), |
340
|
|
|
// We are not using Entity browser's hidden element since we maintain |
341
|
|
|
// selected entities in it during entire process. |
342
|
|
|
'target_id' => [ |
343
|
|
|
'#type' => 'hidden', |
344
|
|
|
'#id' => $hidden_id, |
345
|
|
|
// We need to repeat ID here as it is otherwise skipped when rendering. |
346
|
|
|
'#attributes' => ['id' => $hidden_id], |
347
|
|
|
'#default_value' => implode(' ', array_map( |
348
|
|
|
function (EntityInterface $item) { |
349
|
|
|
return $item->getEntityTypeId() . ':' . $item->id(); |
350
|
|
|
}, |
351
|
|
|
$entities |
352
|
|
|
)), |
353
|
|
|
// #ajax is officially not supported for hidden elements but if we |
354
|
|
|
// specify event manually it works. |
355
|
|
|
'#ajax' => [ |
356
|
|
|
'callback' => [get_class($this), 'updateWidgetCallback'], |
357
|
|
|
'wrapper' => $details_id, |
358
|
|
|
'event' => 'entity_browser_value_updated', |
359
|
|
|
], |
360
|
|
|
], |
361
|
|
|
]; |
362
|
|
|
|
363
|
|
|
// Get configuration required to check entity browser availability. |
364
|
|
|
$cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); |
365
|
|
|
$selection_mode = $this->getSetting('selection_mode'); |
366
|
|
|
|
367
|
|
|
// Enable entity browser if requirements for that are fulfilled. |
368
|
|
|
if (EntityBrowserElement::isEntityBrowserAvailable($selection_mode, $cardinality, count($ids))) { |
369
|
|
|
$persistentData = $this->getPersistentData(); |
370
|
|
|
|
371
|
|
|
$element['entity_browser'] = [ |
372
|
|
|
'#type' => 'entity_browser', |
373
|
|
|
'#entity_browser' => $this->getSetting('entity_browser'), |
374
|
|
|
'#cardinality' => $cardinality, |
375
|
|
|
'#selection_mode' => $selection_mode, |
376
|
|
|
'#default_value' => $entities, |
377
|
|
|
'#entity_browser_validators' => $persistentData['validators'], |
378
|
|
|
'#widget_context' => $persistentData['widget_context'], |
379
|
|
|
'#custom_hidden_id' => $hidden_id, |
380
|
|
|
'#process' => [ |
381
|
|
|
['\Drupal\entity_browser\Element\EntityBrowserElement', 'processEntityBrowser'], |
382
|
|
|
[get_called_class(), 'processEntityBrowser'], |
383
|
|
|
], |
384
|
|
|
]; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
$element['#attached']['library'][] = 'entity_browser/entity_reference'; |
388
|
|
|
|
389
|
|
|
$field_parents = $element['#field_parents']; |
390
|
|
|
|
391
|
|
|
$element['current'] = $this->displayCurrentSelection($details_id, $field_parents, $entities); |
392
|
|
|
|
393
|
|
|
return $element; |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
/** |
397
|
|
|
* Render API callback: Processes the entity browser element. |
398
|
|
|
*/ |
399
|
|
|
public static function processEntityBrowser(&$element, FormStateInterface $form_state, &$complete_form) { |
400
|
|
|
$uuid = key($element['#attached']['drupalSettings']['entity_browser']); |
401
|
|
|
$element['#attached']['drupalSettings']['entity_browser'][$uuid]['selector'] = '#' . $element['#custom_hidden_id']; |
402
|
|
|
return $element; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* {@inheritdoc} |
407
|
|
|
*/ |
408
|
|
|
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { |
409
|
|
|
$entities = empty($values['target_id']) ? [] : explode(' ', trim($values['target_id'])); |
410
|
|
|
$return = []; |
411
|
|
|
foreach ($entities as $entity) { |
412
|
|
|
$return[]['target_id'] = explode(':', $entity)[1]; |
413
|
|
|
} |
414
|
|
|
|
415
|
|
|
return $return; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
/** |
419
|
|
|
* AJAX form callback. |
420
|
|
|
*/ |
421
|
|
|
public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) { |
422
|
|
|
$trigger = $form_state->getTriggeringElement(); |
423
|
|
|
// AJAX requests can be triggered by hidden "target_id" element when |
424
|
|
|
// entities are added or by one of the "Remove" buttons. Depending on that |
425
|
|
|
// we need to figure out where root of the widget is in the form structure |
426
|
|
|
// and use this information to return correct part of the form. |
427
|
|
View Code Duplication |
if (!empty($trigger['#ajax']['event']) && $trigger['#ajax']['event'] == 'entity_browser_value_updated') { |
|
|
|
|
428
|
|
|
$parents = array_slice($trigger['#array_parents'], 0, -1); |
429
|
|
|
} |
430
|
|
|
elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], '_remove_')) { |
431
|
|
|
$parents = array_slice($trigger['#array_parents'], 0, -static::$deleteDepth); |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
return NestedArray::getValue($form, $parents); |
|
|
|
|
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
/** |
438
|
|
|
* {@inheritdoc} |
439
|
|
|
*/ |
440
|
|
|
public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) { |
441
|
|
|
if (($trigger = $form_state->getTriggeringElement())) { |
442
|
|
|
// Can be triggered by "Remove" button. |
443
|
|
|
if (end($trigger['#parents']) === 'remove_button') { |
444
|
|
|
return FALSE; |
445
|
|
|
} |
446
|
|
|
} |
447
|
|
|
return parent::errorElement($element, $violation, $form, $form_state); |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
/** |
451
|
|
|
* Submit callback for remove buttons. |
452
|
|
|
*/ |
453
|
|
|
public static function removeItemSubmit(&$form, FormStateInterface $form_state) { |
454
|
|
|
$triggering_element = $form_state->getTriggeringElement(); |
455
|
|
|
if (!empty($triggering_element['#attributes']['data-entity-id']) && isset($triggering_element['#attributes']['data-row-id'])) { |
456
|
|
|
$id = $triggering_element['#attributes']['data-entity-id']; |
457
|
|
|
$row_id = $triggering_element['#attributes']['data-row-id']; |
458
|
|
|
$parents = array_slice($triggering_element['#parents'], 0, -static::$deleteDepth); |
459
|
|
|
$array_parents = array_slice($triggering_element['#array_parents'], 0, -static::$deleteDepth); |
460
|
|
|
|
461
|
|
|
// Find and remove correct entity. |
462
|
|
|
$values = explode(' ', $form_state->getValue(array_merge($parents, ['target_id']))); |
463
|
|
|
foreach ($values as $index => $item) { |
464
|
|
|
if ($item == $id && $index == $row_id) { |
465
|
|
|
array_splice($values, $index, 1); |
466
|
|
|
|
467
|
|
|
break; |
468
|
|
|
} |
469
|
|
|
} |
470
|
|
|
$target_id_value = implode(' ', $values); |
471
|
|
|
|
472
|
|
|
// Set new value for this widget. |
473
|
|
|
$target_id_element = &NestedArray::getValue($form, array_merge($array_parents, ['target_id'])); |
474
|
|
|
$form_state->setValueForElement($target_id_element, $target_id_value); |
475
|
|
|
NestedArray::setValue($form_state->getUserInput(), $target_id_element['#parents'], $target_id_value); |
476
|
|
|
|
477
|
|
|
// Rebuild form. |
478
|
|
|
$form_state->setRebuild(); |
479
|
|
|
} |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
/** |
483
|
|
|
* Builds the render array for displaying the current results. |
484
|
|
|
* |
485
|
|
|
* @param string $details_id |
486
|
|
|
* The ID for the details element. |
487
|
|
|
* @param string[] $field_parents |
488
|
|
|
* Field parents. |
489
|
|
|
* @param \Drupal\Core\Entity\ContentEntityInterface[] $entities |
490
|
|
|
* Array of referenced entities. |
491
|
|
|
* |
492
|
|
|
* @return array |
493
|
|
|
* The render array for the current selection. |
494
|
|
|
*/ |
495
|
|
|
protected function displayCurrentSelection($details_id, $field_parents, $entities) { |
496
|
|
|
|
497
|
|
|
$field_widget_display = $this->fieldDisplayManager->createInstance( |
498
|
|
|
$this->getSetting('field_widget_display'), |
499
|
|
|
$this->getSetting('field_widget_display_settings') + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')] |
500
|
|
|
); |
501
|
|
|
|
502
|
|
|
$classes = ['entities-list']; |
503
|
|
|
if ($this->fieldDefinition->getFieldStorageDefinition()->getCardinality() != 1) { |
504
|
|
|
$classes[] = 'sortable'; |
505
|
|
|
} |
506
|
|
|
|
507
|
|
|
return [ |
508
|
|
|
'#theme_wrappers' => ['container'], |
509
|
|
|
'#attributes' => ['class' => $classes], |
510
|
|
|
'items' => array_map( |
511
|
|
|
function (ContentEntityInterface $entity, $row_id) use ($field_widget_display, $details_id, $field_parents) { |
512
|
|
|
$display = $field_widget_display->view($entity); |
513
|
|
|
$edit_button_access = $this->getSetting('field_widget_edit') && $entity->access('update', $this->currentUser); |
514
|
|
|
if ($entity->getEntityTypeId() == 'file') { |
515
|
|
|
// On file entities, the "edit" button shouldn't be visible unless |
516
|
|
|
// the module "file_entity" is present, which will allow them to be |
517
|
|
|
// edited on their own form. |
518
|
|
|
$edit_button_access &= $this->moduleHandler->moduleExists('file_entity'); |
519
|
|
|
} |
520
|
|
|
if (is_string($display)) { |
521
|
|
|
$display = ['#markup' => $display]; |
522
|
|
|
} |
523
|
|
|
return [ |
524
|
|
|
'#theme_wrappers' => ['container'], |
525
|
|
|
'#attributes' => [ |
526
|
|
|
'class' => ['item-container', Html::getClass($field_widget_display->getPluginId())], |
527
|
|
|
'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(), |
528
|
|
|
'data-row-id' => $row_id, |
529
|
|
|
], |
530
|
|
|
'display' => $display, |
531
|
|
|
'remove_button' => [ |
532
|
|
|
'#type' => 'submit', |
533
|
|
|
'#value' => $this->t('Remove'), |
534
|
|
|
'#ajax' => [ |
535
|
|
|
'callback' => [get_class($this), 'updateWidgetCallback'], |
536
|
|
|
'wrapper' => $details_id, |
537
|
|
|
], |
538
|
|
|
'#submit' => [[get_class($this), 'removeItemSubmit']], |
539
|
|
|
'#name' => $this->fieldDefinition->getName() . '_remove_' . $entity->id() . '_' . $row_id . '_' . md5(json_encode($field_parents)), |
540
|
|
|
'#limit_validation_errors' => [array_merge($field_parents, [$this->fieldDefinition->getName()])], |
541
|
|
|
'#attributes' => [ |
542
|
|
|
'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(), |
543
|
|
|
'data-row-id' => $row_id, |
544
|
|
|
], |
545
|
|
|
'#access' => (bool) $this->getSetting('field_widget_remove'), |
546
|
|
|
], |
547
|
|
|
'edit_button' => [ |
548
|
|
|
'#type' => 'submit', |
549
|
|
|
'#value' => $this->t('Edit'), |
550
|
|
|
'#ajax' => [ |
551
|
|
|
'url' => Url::fromRoute( |
552
|
|
|
'entity_browser.edit_form', [ |
553
|
|
|
'entity_type' => $entity->getEntityTypeId(), |
554
|
|
|
'entity' => $entity->id(), |
555
|
|
|
] |
556
|
|
|
), |
557
|
|
|
'options' => [ |
558
|
|
|
'query' => [ |
559
|
|
|
'details_id' => $details_id, |
560
|
|
|
], |
561
|
|
|
], |
562
|
|
|
], |
563
|
|
|
'#access' => $edit_button_access, |
564
|
|
|
], |
565
|
|
|
]; |
566
|
|
|
}, |
567
|
|
|
$entities, |
568
|
|
|
empty($entities) ? [] : range(0, count($entities) - 1) |
569
|
|
|
), |
570
|
|
|
]; |
571
|
|
|
} |
572
|
|
|
|
573
|
|
|
/** |
574
|
|
|
* Gets data that should persist across Entity Browser renders. |
575
|
|
|
* |
576
|
|
|
* @return array |
577
|
|
|
* Data that should persist after the Entity Browser is rendered. |
578
|
|
|
*/ |
579
|
|
|
protected function getPersistentData() { |
580
|
|
|
return [ |
581
|
|
|
'validators' => [ |
582
|
|
|
'entity_type' => ['type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')], |
583
|
|
|
], |
584
|
|
|
'widget_context' => [], |
585
|
|
|
]; |
586
|
|
|
} |
587
|
|
|
|
588
|
|
|
/** |
589
|
|
|
* Gets options that define where newly added entities are inserted. |
590
|
|
|
* |
591
|
|
|
* @return array |
592
|
|
|
* Mode labels indexed by key. |
593
|
|
|
*/ |
594
|
|
|
protected function selectionModeOptions() { |
595
|
|
|
return ['append' => $this->t('Append'), 'prepend' => $this->t('Prepend')]; |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
/** |
599
|
|
|
* Provides base for settings summary shared by all EB widgets. |
600
|
|
|
* |
601
|
|
|
* @return array |
602
|
|
|
* A short summary of the widget settings. |
603
|
|
|
*/ |
604
|
|
|
protected function summaryBase() { |
605
|
|
|
$summary = []; |
606
|
|
|
|
607
|
|
|
$entity_browser_id = $this->getSetting('entity_browser'); |
608
|
|
|
if (empty($entity_browser_id)) { |
609
|
|
|
return [$this->t('No entity browser selected.')]; |
610
|
|
|
} |
611
|
|
|
else { |
612
|
|
|
if ($browser = $this->entityTypeManager->getStorage('entity_browser')->load($entity_browser_id)) { |
613
|
|
|
$summary[] = $this->t('Entity browser: @browser', ['@browser' => $browser->label()]); |
614
|
|
|
} |
615
|
|
|
else { |
616
|
|
|
drupal_set_message($this->t('Missing entity browser!'), 'error'); |
617
|
|
|
return [$this->t('Missing entity browser!')]; |
618
|
|
|
} |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
$selection_mode = $this->getSetting('selection_mode'); |
622
|
|
|
$selection_mode_options = EntityBrowserElement::getSelectionModeOptions(); |
623
|
|
|
if (isset($selection_mode_options[$selection_mode])) { |
624
|
|
|
$summary[] = $this->t('Selection mode: @selection_mode', ['@selection_mode' => $selection_mode_options[$selection_mode]]); |
625
|
|
|
} |
626
|
|
|
else { |
627
|
|
|
$summary[] = $this->t('Undefined selection mode.'); |
628
|
|
|
} |
629
|
|
|
|
630
|
|
|
return $summary; |
631
|
|
|
} |
632
|
|
|
|
633
|
|
|
/** |
634
|
|
|
* Determines the entities used for the form element. |
635
|
|
|
* |
636
|
|
|
* @param \Drupal\Core\Field\FieldItemListInterface $items |
637
|
|
|
* The field item to extract the entities from. |
638
|
|
|
* @param array $element |
639
|
|
|
* The form element. |
640
|
|
|
* @param \Drupal\Core\Form\FormStateInterface $form_state |
641
|
|
|
* The form state. |
642
|
|
|
* |
643
|
|
|
* @return \Drupal\Core\Entity\EntityInterface[] |
644
|
|
|
* The list of entities for the form element. |
645
|
|
|
*/ |
646
|
|
|
protected function formElementEntities(FieldItemListInterface $items, array $element, FormStateInterface $form_state) { |
647
|
|
|
$entities = []; |
648
|
|
|
$entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type'); |
649
|
|
|
$entity_storage = $this->entityTypeManager->getStorage($entity_type); |
650
|
|
|
|
651
|
|
|
// Find IDs from target_id element (it stores selected entities in form). |
652
|
|
|
// This was added to help solve a really edge casey bug in IEF. |
653
|
|
|
if (($target_id_entities = $this->getEntitiesByTargetId($element, $form_state)) !== FALSE) { |
654
|
|
|
return $target_id_entities; |
655
|
|
|
} |
656
|
|
|
|
657
|
|
|
// Determine if we're submitting and if submit came from this widget. |
658
|
|
|
$is_relevant_submit = FALSE; |
659
|
|
|
if (($trigger = $form_state->getTriggeringElement())) { |
660
|
|
|
// Can be triggered by hidden target_id element or "Remove" button. |
661
|
|
|
if (end($trigger['#parents']) === 'target_id' || (end($trigger['#parents']) === 'remove_button')) { |
662
|
|
|
$is_relevant_submit = TRUE; |
663
|
|
|
|
664
|
|
|
// In case there are more instances of this widget on the same page we |
665
|
|
|
// need to check if submit came from this instance. |
666
|
|
|
$field_name_key = end($trigger['#parents']) === 'target_id' ? 2 : static::$deleteDepth + 1; |
667
|
|
|
$field_name_key = count($trigger['#parents']) - $field_name_key; |
668
|
|
|
$is_relevant_submit &= ($trigger['#parents'][$field_name_key] === $this->fieldDefinition->getName()) && |
669
|
|
|
(array_slice($trigger['#parents'], 0, count($element['#field_parents'])) == $element['#field_parents']); |
670
|
|
|
} |
671
|
|
|
}; |
672
|
|
|
|
673
|
|
|
if ($is_relevant_submit) { |
674
|
|
|
// Submit was triggered by hidden "target_id" element when entities were |
675
|
|
|
// added via entity browser. |
676
|
|
View Code Duplication |
if (!empty($trigger['#ajax']['event']) && $trigger['#ajax']['event'] == 'entity_browser_value_updated') { |
|
|
|
|
677
|
|
|
$parents = $trigger['#parents']; |
678
|
|
|
} |
679
|
|
|
// Submit was triggered by one of the "Remove" buttons. We need to walk |
680
|
|
|
// few levels up to read value of "target_id" element. |
681
|
|
|
elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], $this->fieldDefinition->getName() . '_remove_') === 0) { |
682
|
|
|
$parents = array_merge(array_slice($trigger['#parents'], 0, -static::$deleteDepth), ['target_id']); |
683
|
|
|
} |
684
|
|
|
|
685
|
|
|
if (isset($parents) && $value = $form_state->getValue($parents)) { |
686
|
|
|
$entities = EntityBrowserElement::processEntityIds($value); |
687
|
|
|
return $entities; |
688
|
|
|
} |
689
|
|
|
return $entities; |
690
|
|
|
} |
691
|
|
|
// IDs from a previous request might be saved in the form state. |
692
|
|
|
elseif ($form_state->has([ |
693
|
|
|
'entity_browser_widget', |
694
|
|
|
$this->getFormStateKey($items), |
695
|
|
|
]) |
696
|
|
|
) { |
697
|
|
|
$stored_ids = $form_state->get([ |
698
|
|
|
'entity_browser_widget', |
699
|
|
|
$this->getFormStateKey($items), |
700
|
|
|
]); |
701
|
|
|
$indexed_entities = $entity_storage->loadMultiple($stored_ids); |
702
|
|
|
|
703
|
|
|
// Selection can contain same entity multiple times. Since loadMultiple() |
704
|
|
|
// returns unique list of entities, it's necessary to recreate list of |
705
|
|
|
// entities in order to preserve selection of duplicated entities. |
706
|
|
|
foreach ($stored_ids as $entity_id) { |
707
|
|
|
if (isset($indexed_entities[$entity_id])) { |
708
|
|
|
$entities[] = $indexed_entities[$entity_id]; |
709
|
|
|
} |
710
|
|
|
} |
711
|
|
|
return $entities; |
712
|
|
|
} |
713
|
|
|
// We are loading for for the first time so we need to load any existing |
714
|
|
|
// values that might already exist on the entity. Also, remove any leftover |
715
|
|
|
// data from removed entity references. |
716
|
|
|
else { |
717
|
|
|
foreach ($items as $item) { |
718
|
|
|
if (isset($item->target_id)) { |
719
|
|
|
$entity = $entity_storage->load($item->target_id); |
720
|
|
|
if (!empty($entity)) { |
721
|
|
|
$entities[] = $entity; |
722
|
|
|
} |
723
|
|
|
} |
724
|
|
|
} |
725
|
|
|
return $entities; |
726
|
|
|
} |
727
|
|
|
} |
728
|
|
|
|
729
|
|
|
/** |
730
|
|
|
* {@inheritdoc} |
731
|
|
|
*/ |
732
|
|
|
public function calculateDependencies() { |
733
|
|
|
$dependencies = parent::calculateDependencies(); |
734
|
|
|
|
735
|
|
|
// If an entity browser is being used in this widget, add it as a config |
736
|
|
|
// dependency. |
737
|
|
|
if ($browser_name = $this->getSetting('entity_browser')) { |
738
|
|
|
$dependencies['config'][] = 'entity_browser.browser.' . $browser_name; |
739
|
|
|
} |
740
|
|
|
|
741
|
|
|
return $dependencies; |
742
|
|
|
} |
743
|
|
|
|
744
|
|
|
/** |
745
|
|
|
* Get selected elements from target_id element on form. |
746
|
|
|
* |
747
|
|
|
* @param array $element |
748
|
|
|
* The form element. |
749
|
|
|
* @param \Drupal\Core\Form\FormStateInterface $form_state |
750
|
|
|
* The form state. |
751
|
|
|
* |
752
|
|
|
* @return \Drupal\Core\Entity\EntityInterface[]|false |
753
|
|
|
* Return list of entities if they are available or false. |
754
|
|
|
*/ |
755
|
|
|
protected function getEntitiesByTargetId(array $element, FormStateInterface $form_state) { |
756
|
|
|
$target_id_element_path = array_merge( |
757
|
|
|
$element['#field_parents'], |
758
|
|
|
[$this->fieldDefinition->getName(), 'target_id'] |
759
|
|
|
); |
760
|
|
|
|
761
|
|
|
if (!NestedArray::keyExists($form_state->getUserInput(), $target_id_element_path)) { |
762
|
|
|
return FALSE; |
763
|
|
|
} |
764
|
|
|
|
765
|
|
|
// TODO Figure out how to avoid using raw user input. |
766
|
|
|
$current_user_input = NestedArray::getValue($form_state->getUserInput(), $target_id_element_path); |
767
|
|
|
if (!is_array($current_user_input)) { |
768
|
|
|
$entities = EntityBrowserElement::processEntityIds($current_user_input); |
769
|
|
|
return $entities; |
770
|
|
|
} |
771
|
|
|
|
772
|
|
|
return FALSE; |
773
|
|
|
} |
774
|
|
|
|
775
|
|
|
} |
776
|
|
|
|
This error could be the result of:
1. Missing dependencies
PHP Analyzer uses your
composer.json
file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects thecomposer.json
to be in the root folder of your repository.Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the
require
orrequire-dev
section?2. Missing use statement
PHP does not complain about undefined classes in
ìnstanceof
checks. For example, the following PHP code will work perfectly fine:If you have not tested against this specific condition, such errors might go unnoticed.